Two things have to be stored, because the thing you look up by is not the thing you want back.
# Lowercasing everything: lookups work, and the output is wrong
store = {'content-type': 'text/plain'}
# sending 'content-type' back is legal and looks like a bug to a human
# Keeping the case: the output is right and lookups miss
store = {'Content-Type': 'text/plain'}
store['content-type'] # KeyError
# Both: the lowercased name is the key, the pair is the value
store = {'content-type': ('Content-Type', 'text/plain')}
- The key is for finding, the value is for using
A dictionary key exists to be matched, so it should be the normalised form; everything the caller wants back — the original spelling, the value — lives on the other side of the colon. Once you see that, case-insensitive lookup stops being a special data structure and becomes an ordinary dict with a slightly richer value.
- One normalising helper, used by every method
Lookup, assignment, membership and deletion all have to agree about what a name means, and the way to guarantee that is for all of them to call the same one-line function. The bug this design prevents is the one where __setitem__ strips whitespace and __contains__ does not, so a header you just set is not there.
- First spelling wins, deliberately
When the same header arrives twice in different cases, something has to decide which spelling survives. Keeping the first is the choice that makes the object stable: re-assigning a value never changes how the headers look, so output does not shift because of an unrelated update.
Keep one dictionary inside, keyed by the lowercased name. Each entry has to carry two things: how the name was written, and the value.
Store {lowercased: (original_name, value)}. Then __getitem__ returns the second half of the pair, __iter__ yields the first halves, and __contains__ checks the lowercased name against the keys.
In __setitem__, look up the lowercased name first: if it is already there, keep the stored spelling and replace only the value. Write get() in terms of __contains__ and __getitem__ rather than reaching into the store again — one definition of what a lookup means.
class Headers: def __init__(self, initial=None): self._store = {} for name, value in (initial or {}).items(): self[name] = value def __setitem__(self, name, value): key = name.____() original = self._store[key][0] if key in self._store else ____ self._store[key] = (original, value) def __getitem__(self, name): return self._store[name.lower()][____] def __contains__(self, name): return name.lower() in self.____ def __iter__(self): for original, _ in self._store.values(): ____ original def __len__(self): return len(self._store) def get(self, name, default=None): return self[name] if name in self else ____
class Headers:
"""Header names that compare without regard to case."""
def __init__(self, initial=None):
# Keyed by the lowercased name; the value carries the spelling
# the author used, so output looks the way they wrote it.
self._store = {}
for name, value in (initial or {}).items():
self[name] = value
def __setitem__(self, name, value):
key = name.lower()
original = self._store[key][0] if key in self._store else name
self._store[key] = (original, value)
def __getitem__(self, name):
return self._store[name.lower()][1]
def __contains__(self, name):
return name.lower() in self._store
def __iter__(self):
for original, _ in self._store.values():
yield original
def __len__(self):
return len(self._store)
def get(self, name, default=None):
return self[name] if name in self else default
Filling the store directly in the constructor would duplicate the normalising rule, and the two copies would drift the first time somebody changed one. Routing construction through the same method every other write uses means there is exactly one place that decides what a header name means.
def __init__(self, initial={}) would evaluate that dictionary once, at class definition time, and every Headers object created without an argument would share it — so one request’s headers would appear in the next. None as the default, filled in inside the body, is the standard fix and worth doing by reflex.
It could reach into self._store and save a lookup. Building it out of __contains__ and __getitem__ instead means it inherits whatever those mean, forever — if the normalising rule gains a strip() tomorrow, get() gets it too, for free. Composing public behaviour out of public behaviour is what keeps a small class consistent.
If you are handling HTTP, the library already has this — requests' CaseInsensitiveDict, http.client’s HTTPMessage, Werkzeug’s Headers — and they handle the parts this does not: repeated headers with the same name (Set-Cookie appears many times and must not overwrite itself), byte-string names, and the folding rules. Write this when the case-insensitive lookup is yours rather than HTTP’s: environment variables on Windows, INI sections, user-supplied column names. And if you want it to behave like a real mapping everywhere, inherit from collections.abc.MutableMapping and get keys(), values(), items(), pop() and equality from the mixin.
- Add __delitem__ and check that deleting by a different case works, then that the name is genuinely gone from the iteration.
- Allow repeated headers, so Set-Cookie can appear twice, and work out what __getitem__ should then return.
- Inherit from collections.abc.MutableMapping and delete every method the mixin gives you for free.