How the parser works

parse() turns a name string into a ParsedName. This page explains the model behind that call: how a string becomes tokens and tokens become fields, how those tokens get their roles, where configuration lives and why it is split the way it is, why parsers are plain values, and what happens when a name is genuinely ambiguous. The task pages all build on these five ideas.

From string to name

Every parse follows the same path: the input string is split into tokens, each token is assigned one of the seven roles — title, given, middle, family, suffix, nickname, maiden — and every string you read off the result is computed from those tokens at read time.

Parsing "Dr. Juan Q. Xavier de la Vega III" produces eight tokens. The first is Dr. with the title role; de, la, and Vega each carry the family role, which is why name.family returns "de la Vega" — the field is a view that joins the family-role tokens in order, not a stored string.

Each token also records where it came from. A Span is a pair of character positions bounding the token in the original string: Dr. has span (0, 3), and name.original[0:3] is exactly "Dr.". Internally, spans let the pipeline refer to a token by position instead of by text. v1 re-found name pieces by searching for matching text, so a name with a repeated word could make the parser rewrite the wrong occurrence (issue #100 and its relatives); a position cannot be confused with a look-alike.

ParsedName is frozen: there is no attribute assignment, ever. If a parse is almost right and you want to fix one field, you call .replace(), which returns a new ParsedName with that field changed and everything else — tokens, spans, the rest of the roles — carried over unchanged. str() renders the default view; nothing about calling it mutates the value you called it on.

Two layers decide the roles

Roles are not assigned in one pass. A vocabulary layer runs first and claims words for what they are, wherever they sit: titles, particles, conjunctions, recognized suffixes, and anything set off by nickname or maiden delimiters. Titles chain, so "Asst. Vice Chancellor" is one title; particles join forward, so de la attaches to Vega.

Whatever the vocabulary layer has not claimed is left to a positional layer, which assigns purely by where a word sits: the first unclaimed word is the given name, the last is the family name, and anything between them is the middle name. name_order and an explicit comma change what “first” and “last” mean here; nothing else does.

This is the whole parser in two sentences, and it explains its character. A word nameparser has never seen still gets a sensible role, because the positional layer does not need to recognize anything. The same word can play different parts in different places — Dr. is a title before a name and a suffix after it, which is why the field names title and suffix are really “pre-nominal” and “post-nominal”. And nothing is statistical: there is no model and no training data, so the same input always parses the same way, and a parse that is wrong is wrong reproducibly, which is what makes it fixable by configuration.

The split also tells you which container a setting belongs in, before you look anything up: if you are teaching the parser a word, it goes in the Lexicon; if you are changing how unclaimed words are arranged, it goes in the Policy.

Configuration lives in three containers

Every piece of nameparser configuration falls into exactly one of three places, and which one is decided by a single question: what does this setting vary with?

  • Lexicon holds everything that varies by language: the vocabulary — titles, particles, suffixes, conjunctions, and the rest of the word lists the parser matches against.

  • Policy holds everything that varies by data source or application: the behavior switches — name order, patronymic rules, delimiters, strip flags — anything that changes how the pipeline runs, not what words it recognizes.

  • Rendering arguments cover everything that varies by output destination: the spec you pass to render(spec), or a keyword to initials()/capitalized().

“Dean” is a common given name, so it is not in the default titles vocabulary. But in some data it is more common as a title. The right reading is a fact about the domain the names come from: that makes it a Lexicon entry. A CRM that wraps nicknames in square brackets — John [Johnny] Smith — instead of quotes is a fact about that one data source’s export format, not about the language of the names in it — that’s a Policy (nickname_delimiters={('[', ']')}). One particular report wanting names formatted as “Family, Given” while every other consumer of the same parsed data wants “Given Family” is a fact about where the string is going next, decided at the moment you render it — that’s a rendering argument, not something baked into how the name was parsed.

This replaces v1’s single Constants object, which mixed all three concerns — vocabulary, behavior, and output formatting — into one mutable bag plus a string_format template string. Sorting a setting into the right container is largely mechanical once you ask the “varies by what?” question above; see Migrating from HumanName for the attribute-by-attribute mapping from the old Constants to the new containers.

Parsers are values

parse() is a convenience function over a module-level default Parser. You only need to build your own Parser (directly, or via parser_for()) when you want non-default vocabulary or behavior — and when you do, build it once and reuse it. Constructing a Parser validates its configuration up front, so it’s cheap but not free; parsing individual names is the hot path, and a Parser is designed to be called many times without reconstruction.

Lexicon, Policy, Parser, and ParsedName are all frozen and hashable. That means they’re safe to share across threads without locking, safe to use as dict keys or cache keys, and equality means exactly what it says — two Parser instances built from equal configuration are equal values, not merely two objects that happen to behave the same. Every piece of configuration in the 2.0 API is a frozen value — including the module-level default parser itself.

Honest ambiguity

Parsing never raises. Pass in a string that doesn’t look like a name at all, and you get back a ParsedName with empty fields, not an exception. The parser’s job is to make a reasonable call on real-world text, not to reject it.

Some calls are irreducibly ambiguous — both readings are legitimate, and no amount of rule-tuning resolves them without breaking some other name. Those surface as entries on ParsedName.ambiguities instead of being silently guessed away. The canonical example: a leading “Van” in “Van Johnson” reads as a given name (that’s the common case for that shape), but “Van” is also a family-name particle in plenty of other names, so the parse records a particle-or-given ambiguity alongside its answer. You can inspect ambiguities to decide, case by case, whether your data needs a second look.

An ambiguity records a decision, not a word. The same token in a different position may present no fork at all: do is in the ambiguous post-nominal vocabulary, but in "Joao da Silva do Amaral de Souza" it sits mid-name, where nothing has to choose between readings — so nothing is recorded. A comma can settle the question before it arises, too: "Ma, Jack" fixes the family name, so the credential reading never comes up, while "John Smith MA" has to call it and says so.

An empty ambiguities is therefore not a certificate of certainty. Reporting is deliberately partial: AmbiguityKind lists the forks worth flagging, and even those are not reported everywhere they occur — the comma paths stay quiet on purpose, since a comma usually settles the structure before the question arises. Coverage grows over releases. Treat a non-empty ambiguities as a signal to act on; do not read an empty one as a guarantee.

Tokens also carry tags — a second, independent label alongside their role, recording how a token was classified rather than what part of the name it belongs to — but only a handful of them are part of the stable API: particle, conjunction, initial, and joined. Any tag written with a namespace prefix, like vocab:..., is provenance information for debugging how a token got classified — it can change shape between releases and isn’t something to match against in your own code. If you need to branch on how a token was classified, branch on role or on one of the four stable tags above, not on a namespaced one.