Pythonic Objects
Notes based on Fluent Python, summarized and rephrased in my own words.[file:17]
The goal: objects that feel built-in
Python’s data model lets user-defined classes behave very naturally: they can integrate with len, repr, iteration, formatting, hashing, and more, just like built-in types.[file:17] We usually don’t inherit from built-in types to get this behavior; instead, we follow duck typing and implement the special methods that a protocol expects.[file:17]
A secondary style tip from the notes: avoid using double leading underscores for most attributes; name-mangling is rarely worth the confusion it causes.[file:17]
Object representations: repr, str, bytes, format
Python has multiple ways to turn an object into a string or bytes:[file:17]
repr(obj)callsobj.__repr__()and should return a detailed, unambiguous representation aimed at developers.str(obj)callsobj.__str__()and should return a user-friendly, readable representation.bytes(obj)callsobj.__bytes__()if defined, to get abytesrepresentation.format(obj, spec)and"{obj:spec}".format(obj=...)callobj.__format__(spec).[file:17]
In Python 3:
__repr__,__str__, and__format__must return Unicode strings (str).- Only
__bytes__should return abytesobject.[file:17]
A good rule of thumb:
repr(x)should, when possible, look like valid Python code to reconstructxor at least clearly show its type and important data.str(x)can be simpler, focused on how humans would like to see it.
A 2D vector class using the data model
A simple Vector2d class can show how to hook into several data model methods:[file:17]
from array import array
import math
class Vector2d:
typecode = 'd'
def __init__(self, x, y):
self.x = float(x)
self.y = float(y)
def __iter__(self):
# allows tuple(self), unpacking, and for-loops
return (i for i in (self.x, self.y))
def __repr__(self):
class_name = type(self).__name__
return '{}({!r}, {!r})'.format(class_name, *self)
def __str__(self):
# nice, readable form
return str(tuple(self))
def __bytes__(self):
# custom binary representation: typecode + raw doubles
return bytes([ord(self.typecode)]) + bytes(array(self.typecode, self))
def __eq__(self, other):
# structural equality
return tuple(self) == tuple(other)
def __abs__(self):
# vector magnitude
return math.hypot(self.x, self.y)
def __bool__(self):
# truthiness based on magnitude
return bool(abs(self))Thanks to these methods:
- We can print vectors nicely, compare them, and treat them as truthy/falsy based on their length.[file:17]
- Converting to
tuple(v)or unpackingx, y = vjust works because of__iter__.[file:17] bytes(v)gives a compact binary form that can later be used to reconstruct the object.[file:17]
Alternate constructors with @classmethod
If we can turn a Vector2d into bytes, it’s natural to want a constructor that rebuilds it from bytes.[file:17] This is a classic use case for @classmethod:
class Vector2d:
typecode = 'd'
# ... other methods ...
@classmethod
def frombytes(cls, octets):
typecode = chr(octets[0])
memv = memoryview(octets[1:]).cast(typecode)
return cls(*memv)Key points:[file:17]
@classmethodchanges the first parameter of the method to be the class (cls) instead of an instance.- This lets you define alternate constructors that return
cls(...), so subclasses automatically get the right type when they callfrombytes.
classmethod vs staticmethod
Both decorators change how methods are called:[file:17]
@classmethodreceives the class as its first argument and is mostly used for alternate constructors or operations that conceptually apply to the class as a whole.@staticmethodreceives no special first argument and behaves just like a regular function that happens to be placed inside a class.[file:17]
Example:[file:17]
class Demo:
@classmethod
def klassmeth(*args):
return args
@staticmethod
def statmeth(*args):
return args
Demo.klassmeth() # (Demo,)
Demo.klassmeth('spam') # (Demo, 'spam')
Demo.statmeth('spam') # ('spam',)In practice, @classmethod is often genuinely useful, while many Pythonists prefer to define helper functions at module level instead of using @staticmethod, unless grouping them inside the class significantly improves readability.[file:17]
Custom formatting with __format__
format(obj, spec) and f-strings like f"{obj:spec}" delegate to obj.__format__(spec).[file:17]
Examples with built-in types:[file:17]
br1 = 1 / 2.43
format(br1, '0.4f') # '0.4115'
'1 BRL = {rate:0.2f} USD'.format(rate=br1) # '1 BRL = 0.41 USD'
format(42, 'b') # '101010' (binary)
format(2/3, '.1%') # '66.7%'
from datetime import datetime
now = datetime.now()
format(now, '%H:%M:%S') # '17:27:40'
"It's now {:%I:%M %p}".format(now) # e.g. "It's now 05:27 PM"If your class does not implement __format__, it inherits the default from object, which usually just calls str(obj).[file:17]
Making Vector2d hashable
To use custom objects as keys in dicts or members of sets, they must be hashable:
- Implement a stable
__hash__method. - Implement
__eq__consistently with__hash__.[file:17]
For Vector2d, we can implement read-only attributes and a simple hash:
class Vector2d:
typecode = 'd'
def __init__(self, x, y):
self.__x = float(x)
self.__y = float(y)
@property
def x(self):
return self.__x
@property
def y(self):
return self.__y
def __iter__(self):
return (i for i in (self.x, self.y))
def __hash__(self):
return hash(self.x) ^ hash(self.y)Notes:[file:17]
- The double-underscore attributes
__xand__yare made read-only via properties, so the vector’s coordinates cannot be mutated after creation. - A hash is computed by XOR-ing the hashes of the components; any stable combination function is acceptable as long as equal vectors produce the same hash.[file:17]
- Once an instance is used as a dict key or set element, its hash must not change during its lifetime.
Python “private” and “protected” attributes
Python does not have strict access control like private in Java or C++.[file:17] However, it does have two conventions / mechanisms:
- Single leading underscore (e.g.
_x): "this is internal / non-public" by convention. Tools and readers treat it as "protected" implementation detail.[file:17] - Double leading underscores (e.g.
__mood): triggers name mangling to avoid accidental attribute clashes in subclasses.[file:17]
Name mangling rules:[file:17]
- An attribute named
__moodin classDogis stored as_Dog__moodin the instance’s__dict__. - In a subclass
Beagle, its own__moodwould be stored as_Beagle__mood, avoiding collisions withDog’s internal attribute.[file:17]
This is mainly to prevent accidental conflicts, not to provide real security:[file:17]
- You can still access "private" attributes by using the mangled name explicitly (e.g.
v1._Vector2d__x). - That’s sometimes useful for debugging or serialization.
Many Python developers prefer a simpler convention: use a single underscore to mark internal attributes and avoid double underscores unless you really need name-mangling.[file:17]
Saving memory with __slots__
By default, each instance stores its attributes in a per-instance dictionary named __dict__. Dictionaries are flexible but have non-trivial memory overhead.[file:17]
If you have huge numbers of instances with only a few attributes, you can define __slots__ on the class to store attributes in a more compact structure (roughly like a fixed tuple) instead of a dict:[file:17]
class Vector2d:
__slots__ = ('__x', '__y')
typecode = 'd'
# other methods hereCaveats:[file:17]
- Every subclass must define its own
__slots__if you want the effect to propagate; inherited__slots__are ignored. - Instances cannot have attributes beyond those listed in
__slots__, unless you explicitly include'__dict__'in__slots__(but then you lose memory savings). - If you need instances to be weak-referenceable, you must also include
'__weakref__'in__slots__.[file:17]
So __slots__ is a performance/memory optimization to be used only when profiling shows it matters.
Overriding class attributes with instance attributes
A small but useful Python trait: class attributes act as shared defaults for instances.[file:17]
- If you access
obj.attrandattris not found in the instance’s__dict__, Python looks it up on the class, and then in base classes. - Assigning
obj.attr = valuecreates/updates an instance attribute that shadows the class attribute.
This lets you define sensible defaults at the class level while still allowing per-instance overrides when needed.