Immutability & const
SeniorThe built-in immutable types, shallow vs deep immutability, typing.Final and Enum, and whether a custom object can truly be made immutable.
Created 12 September 2026 · Updated 23 October 2026
8 concepts · detail hidden
01The built-in immutable types — what operation each one disallows
#c-the-built-in-immutable-types-what-operation-each-one-disalloint,float,complex,bool,str,tuple,frozenset,bytes, andNoneType(trivially immutable, since there’s only ever one possible value,None, in the first place). Each one specifically disallows genuine in-place mutation of its contents:strhas no__setitem__at all (unlike alist, where item assignment is normal);tuplehas neither.append()nor__setitem__(again, unlikelist);frozensethas neither.add()nor.remove()(unlikeset). This design choice matters beyond mere definition: immutability is precisely what makesstr/tuple/frozensetgenuinely hashable, and therefore safely usable as dict keys or set members — a mutable object generally can’t safely serve that role, since its hash could change after insertion, breaking the container’s internal invariants.02Shallow immutability — what a `tuple` containing a `list` does and doesn't guarantee
#c-shallow-immutability-what-a-tuple-containing-a-list-does-andA tuple guarantees only that its own references — which specific objects occupy which positions — can never be reassigned, added, or removed after creation (
my_tuple[0] = xis simply disallowed, and the tuple can never be resized). But if one of those references happens to point at a genuinely mutable object, such as a nestedlist, that inner object remains fully mutable through the tuple just as it would anywhere else —my_tuple[0].append(x)works perfectly fine, silently mutating the list “inside” what people often loosely call an “immutable” tuple. This is precisely shallow immutability, not full deep/recursive immutability — a genuinely important distinction many people gloss over, and a real, common source of “but I thought tuples were supposed to be immutable!” confusion and bugs.03Why Python has no runtime `const` — `typing.Final` and the `UPPER_CASE` convention
#c-why-python-has-no-runtime-const-typingfinal-and-the-upper_caThe honest, direct answer is no — Python has genuinely no runtime-enforced constant binding mechanism at all. The widespread
UPPER_CASEnaming convention for “constants” is purely a social/style convention (per PEP 8), carrying absolutely no enforcement from the interpreter itself whatsoever — nothing whatsoever stopsMY_CONSTANT = "a completely new value"from succeeding silently, exactly like any other ordinary reassignment.typing.Final(3.8+) is the closest thing Python offers — but consistent with a recurring pattern throughout thetypingmodule, it’s genuinely static-only: checked by tools like mypy/pyright, but doing absolutely nothing whatsoever to prevent actual reassignment at runtime. Assuming Python genuinely enforces this at runtime is a common but incorrect assumption.04Making a custom class immutable — the mechanisms, and why `object.__setattr__` bypasses all of them
#c-making-a-custom-class-immutable-the-mechanisms-and-why-objecRanked roughly by genuine robustness:
NamedTuplegives fields that genuinely, structurally cannot be reassigned, backed by exactly the same underlying guarantees as ordinary tuple immutability.dataclass(frozen=True)overrides__setattr__/__delattr__to actively raiseFrozenInstanceErroron any attempted assignment — genuinely robust against accidental misuse, but still fully bypassable by anyone deliberately callingobject.__setattr__(instance, 'x', value)directly — meaning it’s “immutable against accidents,” not “immutable against a genuinely determined circumvention attempt.” A fully hand-rolled version, overriding__setattr__yourself to unconditionally raise, has exactly this same fundamental bypass still available viaobject.__setattr__. The honest overall conclusion: Python simply has no mechanism that’s genuinely immutable against a sufficiently determined caller willing to reach forobject.__setattr__or direct__dict__manipulation — every “immutable” custom class in Python is immutable by strong convention and friction, not by a truly unbreakable guarantee, unlike a language offering genuine, lower-level memory-protectedconstsemantics.from dataclasses import dataclass, FrozenInstanceError @dataclass(frozen=True) class Money: amount: int currency: str m = Money(10, "EUR") try: m.amount = 20 # generated __setattr__ raises except FrozenInstanceError as e: print(e) # -> cannot assign to field 'amount' object.__setattr__(m, "amount", 20) # bypasses the override entirely print(m) # -> Money(amount=20, currency='EUR')The
frozen=Truemechanism is just generated__setattr__/__delattr__methods on the class, which is why going one level up the MRO toobject.__setattr__skips them. The dataclass machinery itself uses exactly this bypass inside the generated__init__(and you use it in__post_init__to set derived fields on a frozen instance), so it’s a legitimate escape hatch for construction, not just an attack.05`Enum` vs module-level constants — a real type for a closed set of named values
#c-enum-vs-module-level-constants-a-real-type-for-a-closed-set-Enumgives you a genuine, distinct type representing a fixed, closed set of named values — meaning a function can meaningfully type-annotate and validate “this parameter must be one of these specificStatusmembers,” something plain module-level constants simply have no way to express as a genuine type constraint at all. It also gives you convenient iteration over all defined members, and — worth noting for completeness — it still doesn’t fundamentally prevent someone from monkey-patching a new attribute onto the enum class after the fact, or doing something genuinely unwise at a deeper metaclass level in unusual setups; but for the ordinary, everyday case, it remains a meaningfully stronger, more type-safe tool for expressing “this represents a closed, named set of options” than a handful of loose bare constants ever could be.06Preventing subclassing, instance-attribute addition, and class monkey-patching — what's possible
#c-preventing-subclassing-instance-attribute-addition-and-classPreventing subclassing specifically: yes, this works genuinely cleanly — a metaclass (or, more simply since 3.6,
__init_subclass__deliberately raising an error) can straightforwardly reject any attempt whatsoever to subclass. Preventing arbitrary new attribute addition specifically to instances:__slots__blocks genuinely new instance attributes from being created, though notably it does nothing to prevent reassignment of attributes that already legitimately exist. Preventing monkey-patching of the class itself (replacing or adding a method directly on the class, after its original definition) is genuinely harder, and far less commonly attempted at all — Python’s classes are, by deliberate design, themselves fully mutable objects (their own__dict__, while presented externally as a read-onlymappingproxy, remains genuinely replaceable at the class level viatype.__setattr__) — and there’s simply no clean, standard, idiomatic “seal this class entirely” mechanism in the everyday toolkit, the way__slots__cleanly covers the analogous instance-attribute case.08Practices teams use to approximate `const` safety without language support
#c-practices-teams-use-to-approximate-const-safety-without-langThere’s no single silver bullet; teams typically layer several practices: static type checking using
Finalannotations, enforced in CI even though it provides zero runtime protection on its own; code-review conventions and automated linters specifically flagging any reassignment ofUPPER_CASE-named identifiers; reaching deliberately forfrozen=Truedataclasses orNamedTuplespecifically for genuine value objects (consciously accepting theobject.__setattr__escape hatch as a tolerable residual risk rather than a true, unbreakable guarantee); and simply, honestly, not needing this particular guarantee nearly as often in practice as a statically-const-enforcing language’s culture might suggest — since Python’s broader dynamic-by-default culture tends to lean on genuine engineering discipline and thorough tests more heavily than on compiler-enforced structural guarantees in general.