Repository navigation
Remove empty_attribute_default; empty attributes always return '' #255
Description
Summary
2.0 removes the empty_attribute_default config option entirely. Empty name attributes always return ''. Once None support goes (see below for why it must), the only legal value left is the default — a dial with one position isn't configuration.
History: why it exists
Added in v0.3.15 (2016, pre-1.0) for #44: a user wanted None instead of '' to store name components as database NULLs. The maintainer's first response was the one-liner that is now the migration path — "you could do it easily too with something like name.title or None" — and "I don't have a strong preference." Making None the universal return broke the formatting tests, so a config option was added as a compromise. The first consequence arrived in the same thread: str() produced 'None John None Doe None (None)', patched in PR #45 with a .replace(str(empty_attribute_default), '') scrub that is still in __str__ today.
What it has cost
- Every
-> strannotation on the public API is false when the option is set toNone; the attribute was deliberately left untyped in the mypy work (PR Add tests/ to mypy scope; fix real type gaps it surfaces #250) because typing it honestly cascades| Nonethrough everything - The
"J. None D."initials bug, fixed in 1.3.0 — same laundering class - Live data corruption (str() strips literal "None" from real names when empty_attribute_default is None #254): the PR remove default attribute from formatted string (#44) #45 scrub can't distinguish an interpolated
Nonefrom name text, so in None-modestr(HumanName("Nonez Smith"))returns'z Smith'— the parse is correct and__str__strips the literalNoneout of the person's name
What removal deletes
The or self.C.empty_attribute_default tail on all seven attribute properties plus surnames/given_names/last_base/last_prefixes and initials(); the __str__ scrub hack; the docs section; the test type: ignores.
Removal mechanics (both learned from the 1.3.0 guard work)
- Tombstone, not deletion:
empty_attribute_defaultis a plain attribute, so naive removal makesCONSTANTS.empty_attribute_default = Nonea silent no-op — the accept-and-ignore failure family Non-descriptor Constants set attributes accept bare-string assignment; substring 'in' silently corrupts parsing #241 exterminated. 2.0 keeps a property whose setter raises with the migration hint. - Readers exist, not just writers: the Option to get None instead of empty string #44 thread advised asserting against
hn.C.empty_attribute_defaultin downstream tests. Reads break loudly with the tombstone; migration is "compare with''or truthiness."
Migration
hn.first or None at the database boundary, or {k: v or None for k, v in name.as_dict().items()} for a whole record.
Bridge (1.4) — tracked in #263
-
DeprecationWarningon any assignment toempty_attribute_default - Ship the
__str__mangling fix (str() strips literal "None" from real names when empty_attribute_default is None #254) so None-mode users get correct output during their migration window
Activity
derek73 commentedon Jul 11, 2026
Note from the review of #277 (the 1.3.1 fix for #254): the fix changed
str()behavior for custom truthy values ofempty_attribute_default, an undocumented configuration (only''andNoneare documented or tested).With
empty_attribute_default = 'N/A':str(HumanName('John Smith'))→'John Smith'— the post-format scrub deleted the placeholder from empty slots, but also corrupted real name text containing it ('N/A Smith'→'Smith', the same bug class as str() strips literal "None" from real names when empty_attribute_default is None #254)'N/A John N/A Smith N/A (N/A)'— thev or ''substitution only blanks falsy values, so a truthy default now renders literally in formatted outputDecision was to leave this as-is rather than complicate the deprecation-window code, since this option is removed entirely here in 2.0. If anyone reports depending on a truthy default before then, the fix would be to key the
__str__substitution off actual field emptiness (the_listattributes) instead of value falsiness.🤖 Generated with Claude Code
Add 2.0 deprecation warnings for 1.4 second-wave removals (#263) (#286)
Release v1.4.0 (#287)
M12 batch 1: drop the dead empty_attribute_default dual-run fixture (…
Reconcile test_python_api against the facade (buckets A, B, E)
Reconcile test_constants against the shim (buckets A, B, C)
Reconcile test_output_format against the facade (buckets B, C)
Reconcile test_initials against the facade (buckets B, C, E)
Reconcile test_nicknames (buckets A, B); hold on a bucket-precedence …
Reconcile test_variations against the facade (buckets A, B)
docs: rewrite the 2.0 release log new-API-first and complete the audit
Delete the dead autouse override in tests/v2/conftest.py
derek73 commentedon Jul 28, 2026
Shipped in 2.0.0. Delivery and release-log coverage were verified issue-by-issue in the pre-release milestone audit.