A Pythonic Object, part 2
Source: Fluent Python, 2nd Edition — Chapter 11 by Luciano Ramalho (formatted displays through class attributes)
Part 1 built a vector that can be printed, unpacked, and reconstructed. This half is about how it is displayed (__format__), whether it can live in a set (__hash__ plus immutability), positional match, name mangling, __slots__ inheritance, and using a class attribute as a default you can shadow.
f'{x:0.2f}', format(x, '0.2f'), and '{0:0.2f}'.format(x) all call __format__(format_spec). The specifier is only the text after the colon. The name or expression before the colon is a different language: field names for .format(), or any expression inside an f-string.
'{0.mass:5.3e}'
^^^^^^ ^^^^
field format spec (mini-language)
Study format(obj, spec) first. Then read formatted string literals and format-string syntax for {} fields, including !s, !r, and !a. F-strings do not retire str.format(): keep the template in a variable when the layout is not next to the values.
Built-in presentation codes
Each type interprets format_spec itself. Integers understand b and x. Floats understand f and %. datetime reuses strftime codes inside __format__.
🧪 format codes and datetime
If a class never defines __format__, object.__format__ returns str(obj) when the spec is empty. A non-empty spec raises TypeError. A vector that already has __str__ still needs __format__ before format(v, '.3f') works.
Format each component
A first __format__ can treat the user’s spec as “apply this to every float in the vector, then wrap with parentheses.”
def __format__(self, fmt_spec=''):
parts = (format(c, fmt_spec) for c in self)
return '({}, {})'.format(*parts)
That yields format(v, '.2f') → (3.00, 4.00) and format(v, '.3e') for scientific notation on each axis.
What is the specifier?
1 / 1In f'1 USD = {1 / brl:0.2f} BRL', the format specifier is
Pick an answer to see if you are right.
A custom code: polar p
The mini-language is per-class. Integers already use bcdoxXn, floats eEfFgGn%, strings s. A letter unused by those types is easier for humans: p for polar.
If the spec ends with p, strip that letter, format magnitude (abs) and angle (math.atan2(y, x)) with the remainder of the spec, and wrap with <{}, {}>. Otherwise keep Cartesian (x, y).
def angle(self):
return math.atan2(self.y, self.x)
def __format__(self, fmt_spec=''):
if fmt_spec.endswith('p'):
fmt_spec = fmt_spec[:-1]
coords = (abs(self), self.angle())
outer = '<{}, {}>'
else:
coords = self
outer = '({}, {})'
parts = (format(c, fmt_spec) for c in coords)
return outer.format(*parts)
Reusing a letter another type uses is not a syntax error (each class owns its spec), but it confuses readers. Pick unused letters when you can.
Hashable vectors need a stable identity
hash(v) and set([v]) fail until __hash__ exists. Equal objects must share a hash, so mix the same fields __eq__ uses. XOR of component hashes is the usual small example:
def __hash__(self):
return hash(self.x) ^ hash(self.y)
Hashes are only honest if the value cannot change after it is used as a key. Store self.__x / self.__y, expose @property getters named x and y, iterate via those properties. Assignment v.x = 7 then raises AttributeError.
You can still hash a type that is technically mutable if you never mutate it. Properties are the teaching device, not a language requirement.
If the type has a natural scalar, you might add __int__, __float__, or __complex__. Mapping a 2-D vector to complex is a reasonable extra; this lesson does not require it.
Positional match needs __match_args__
Keyword patterns such as case Vector2d(x=0, y=0): already work if those names are public attributes. A positional pattern case Vector2d(_, 0): fails with “accepts 0 positional sub-patterns” until the class lists order:
class Vector2d:
__match_args__ = ('x', 'y')
You do not have to list every public field. Required constructor arguments are the usual tuple; optional extras can stay keyword-only in patterns.
Private names are mangled, not locked
Python has no Java-style private. Two leading underscores (and at most one trailing underscore) trigger name mangling: Dog.__mood is stored as _Dog__mood, Beagle.__mood as _Beagle__mood. Subclasses do not accidentally overwrite the parent’s internal name.
That is a safety cover, not a lock. Anyone can read v._Vector2d__x or assign it. Production code that pokes mangled names should not be surprised when the class breaks.
Many Pythonistas prefer a single underscore: self._x. The interpreter does not mangle it. The community treats it as “do not touch from outside.” Some docs call this “protected”; the word is informal.
Vector2d components are “private” and instances are “immutable” only in scare quotes. There is no real privacy and no real freeze.
Name mangling
1 / 2After Vector2d stores self.__x, v.__dict__ typically contains
Pick an answer to see if you are right.
__slots__: cheaper storage, inherited halfway
By default each instance has a __dict__. A class attribute __slots__ = ('x', 'y') (set when the class is created) stores those names in a compact layout instead. Effects:
- No
__dict__unless you put'__dict__'in the slot list - Setting an unknown name raises
AttributeError - Subclasses without their own
__slots__get a__dict__again; slot names from the parent still live in the compact layout, extras go in the dict - Empty
__slots__ = ()on the subclass: no dict, only parent slot names - Extra names: declare them in the subclass
__slots__(remember the comma in('color',)) '__dict__'in slots allows dynamic attributes and@cached_property, and can erase the memory win'__weakref__'is needed if you both use slots and need weak references
Millions of numeric records still belong in NumPy, not a slotted Python class. Slots are a tool for when you really have many small objects of one shape.
🧪 slots inheritance
Class attributes as defaults you can shadow
typecode = 'd' on the class is the default self.typecode because instances do not store that name. Writing v.typecode = 'f' creates an instance attribute and leaves Vector2d.typecode as 'd'. Byte dumps then use 4-byte 'f' floats for that one object.
To change the default for a whole family, assign on the class, or — clearer — subclass:
class ShortVector2d(Vector2d):
typecode = 'f'
Django class-based views do this a lot: a tiny subclass whose only job is a different class attribute.
That is why __repr__ should use type(self).__name__ instead of a hardcoded 'Vector2d'. ShortVector2d then shows the right constructor name without copying __repr__.
You only implement these dunders when the application or the library audience needs them. End users of a GUI do not care. Other Python programmers using your types will try format, hash, match, and subclasses.