Python Type Casting With int, float, and str
In Python, type casting is not a low-level memory reinterpretation,
but an object-oriented instantiation process driven by built-in class
constructors like int(), float(), and
str(). When you cast a value using one of these functions,
Python evaluates the input, checks for compatible internal protocols via
special "dunder" methods, or passes the data to specialized CPython
conversion algorithms. This article explains how Python implements type
casting under the hood, detailing the roles of the data model protocols,
string parsing routines, and fallback mechanisms.
Constructors Instead of Traditional Casts
Unlike compiled languages like C or C++, where casting often
reinterprets raw bits in memory or instructs the compiler to handle an
address differently, Python treats int, float,
and str as built-in classes.
Calling int(x) is syntactically identical to
instantiating an object of class int. When invoked, the
type's __new__ and __init__ methods are
triggered. The constructor inspects the type of argument x
and selects the appropriate internal execution path: delegation to an
object's protocol method or invocation of low-level string parsing
routines.
The Protocol-Based Delegation Model
Python relies heavily on duck typing and the Python Data Model. When a built-in constructor receives an existing object, it attempts to call a corresponding magic method defined on that object's class.
1. How int() Works
When int(x) is called:
- If
xis already an integer, Python returnsxdirectly (or a new reference to it). - Python checks if
ximplements the__int__()method. If present, it executesx.__int__(). The returned value must be of typeint; otherwise, aTypeErroris raised. - If
__int__()is not defined, Python checks for__index__(). This method is designed for objects that can serve as sequence indices (such as custom integer-like types) and must return an integer. - If
xis a string or bytes-like object, Python bypasses protocol methods and invokes the internal C-level parsing functionPyLong_FromString. This function parses string characters, strips surrounding whitespace, checks for optional base prefixes (like0xor0b), and constructs an arbitrary-precision integer. - If none of these conditions are met, Python raises a
TypeError.
2. How float() Works
When float(x) is executed:
- Python checks for the
__float__()method on the object. If defined, it callsx.__float__(). - If
__float__()is missing, Python falls back to__index__()to convert integer-like objects into floating-point numbers. - If
xis a string, CPython callsPyFloat_FromString. This function parses numeric characters, decimal points, exponents (e.g.,1e-4), and case-insensitive string literals such as"infinity","inf", and"nan". - If the string cannot be parsed into a valid IEEE 754 floating-point
number, a
ValueErroris raised.
3. How str() Works
When str(x) is executed:
- Python prioritizes the
__str__()method. It callsx.__str__(), which is intended to return an informal, human-readable representation of the object. - If the class does not define
__str__(), Python falls back tox.__repr__(). Because all Python objects inherit from the baseobjectclass, a default__repr__()is always available (typically outputting<ClassName object at 0x...>). - At the C level,
str()delegates toPyObject_Str(), which safely ensures the result is an instance of the unicode string type (PyUnicodeObject).
Low-Level Parsing and Numeric Conversion
When converting strings to numbers, Python cannot rely on dunder methods because strings do not intrinsically know how to compute arbitrary numeric representations. Instead, CPython relies on internal C routines:
- Base-N String Parsing (
int): ThePyLong_FromStringalgorithm iterates through the characters, verifying each character against the allowed digit set for the specified radix (base 10 by default). It dynamically allocates memory for Python’s variable-lengthPyLongObjectstructure to prevent arithmetic overflow. - Float Parsing (
float): CPython uses optimized floating-point parsing algorithms (historically based on David Gay'sdtoa.c) to read string buffers and produce correctly rounded 64-bit double-precision IEEE 754 values.
Implementing Casting in Custom Classes
Because type casting relies on the Python Data Model, user-defined
classes can define how they behave when passed to int(),
float(), and str():
class Temperature:
def __init__(self, celsius):
self.celsius = float(celsius)
def __int__(self):
return int(self.celsius)
def __float__(self):
return self.celsius
def __str__(self):
return f"{self.celsius}°C"
temp = Temperature(21.8)
print(int(temp)) # Output: 21 (calls __int__)
print(float(temp)) # Output: 21.8 (calls __float__)
print(str(temp)) # Output: 21.8°C (calls __str__)Error Handling
Python enforces strict type safety during casting operations:
TypeError: Occurs when an argument cannot be converted because it does not define the appropriate protocol method (e.g.,int(None)).ValueError: Occurs when an argument has the correct type (like a string) but contains invalid data for the conversion (e.g.,int("abc")orfloat("12.34.56")).