Implementing Numeric Tower
Pythonfrom numbers import Integral, Rational, Real, Complex, Number from fractions import Fraction # Check numeric type generically instead of using int/float/complex directly def process(x): if isinstance(x, Integral): return x << 1 # bitwise ops only valid for Integral elif isinstance(x, Rational): return x.numerator / x.denominator elif isinstance(x, Real): return float(x) elif isinstance(x, Complex): return x.real, x.imag raise TypeError(f"{x!r} is not a number") process(3) # Integral process(Fraction(1, 2)) # Rational process(3.14) # Real process(2 + 3j) # Complex
Number
└── Complex
└── Real
└── Rational
└── Integral
Each ABC is a superclass of the one below it. Built-in registrations:
| ABC | Registered built-ins |
|---|---|
Complex | complex |
Real | float |
Rational | fractions.Fraction |
Integral | int |
int is also Real, Complex, and Number (inherited transitively). No built-in implements Rational except Fraction.
Progress:
- Step 1: Decide which ABC matches the semantics of your custom type
- Step 2: Subclass that ABC directly (don't just duck-type)
- Step 3: Implement all abstract methods required by that ABC (see table below)
- Step 4: Decide
__eq__/__hash__semantics consistent with numeric equality - Step 5: Test
isinstance()against the ABC and against mixed-type arithmetic - Step 6: If wrapping a third-party type you can't subclass, use
ABC.register()
Required abstract methods per ABC
Complex
__complex__,real,imag,__add__,__radd__,__neg__,__pos____mul__,__rmul__,__truediv__,__rtruediv__,__pow__,__rpow____abs__,__eq__
Real (adds to Complex)
__float__,__trunc__,__floor__,__ceil__,__round____floordiv__,__rfloordiv__,__mod__,__rmod____lt__,__le__- (
real/imagbecome trivial:real = self,imag = 0)
Rational (adds to Real)
numerator,denominator(as properties)- Default
__float__is provided asnumerator / denominator
Integral (adds to Rational)
__int__,__index____pow__with optional modulo third argument- Bitwise ops:
__lshift__,__rshift__,__and__,__xor__,__or__,__invert__ - Default
numerator = self,denominator = 1
Example 1: Registering a third-party type without subclassing
Input: You have mpmath.mpf or a custom C-extension float-like type you cannot modify to subclass Real.
Output:
Pythonfrom numbers import Real Real.register(mpmath.mpf) isinstance(mpmath.mpf(1.5), Real) # True issubclass(mpmath.mpf, Real) # True
Note: register() grants isinstance/issubclass compatibility only — it does NOT provide any of the mixin methods or operator implementations. The type must already implement the needed behavior itself.
Example 2: Choosing the right ABC for a type check
Input: Writing a function def scale(x, factor) that should accept any number type but reject non-numeric input, and must support both integer bit-shifting fast paths and general float fallback.
Output:
Pythonfrom numbers import Integral, Complex def scale(x, factor): if not isinstance(x, Complex): raise TypeError("x must be numeric") if isinstance(factor, Integral) and factor >= 0: # fast path only valid for non-negative integers return x * (1 << factor) return x * (2 ** factor)
Example 3: Implementing a minimal custom Rational-like type
Input: A Money class representing exact cents that should behave like a Rational.
Output:
Pythonfrom numbers import Rational from math import gcd class Money(Rational): def __init__(self, cents, denom=1): g = gcd(cents, denom) or 1 self._num = cents // g self._den = denom // g @property def numerator(self): return self._num @property def denominator(self): return self._den def __add__(self, other): return Money(self._num * other.denominator + other.numerator * self._den, self._den * other.denominator) def __radd__(self, other): return self.__add__(other) # ... implement remaining Complex/Real abstract methods ...
- Check against the ABC, never the concrete type. Use
isinstance(x, Integral)instead ofisinstance(x, int)sonumpyints,Fractions used as integers, etc., aren't rejected. - Pick the most specific ABC that matches semantics, not the most permissive. If your type has no meaningful ordering or is inherently complex-valued, stop at
Complex— don't forceReal. - Prefer subclassing over
register()when you control the code; subclassing gives you the default mixin implementations (e.g.,Rational.__float__) for free. Useregister()only for types you can't modify. hash()consistency: numbers that compare equal (e.g.1 == 1.0 == Fraction(1,1) == (1+0j)) should hash equally where the type supports hashing at all.- Implement
__eq__to accept cross-tower comparisons (e.g. yourRationaltype should equal a plainintorfloatwith the same value), mirroring howFraction(2,1) == 2.0works.
- Registering a type without implementing arithmetic:
register()only affectsisinstance/issubclass; calling+,-, etc. on the registered type still fails unless those dunder methods actually exist. - Subclassing
Integralbut forgetting__index__: without it, your type can't be used as a sequence index or inrange(),bin(), etc. - Assuming
Rationalimplies exact base-10 decimal representation — it means exact numerator/denominator representation, not decimal precision (that'sdecimal.Decimal, which deliberately does NOT register under this tower'sReal/Rationaldue to differing semantics around exactness/precision). - Comparing
Decimalandfloat/Fractionvia the tower:decimal.Decimalis intentionally not part of the numeric ABC tower (not registered asReal), soisinstance(Decimal("1.5"), Real)isFalse— don't assume it participates. - Forgetting
__hash__when defining__eq__: Python sets__hash__toNoneif you override__eq__without also defining__hash__, silently breaking use in sets/dicts.