Phantom Types for Python
phantom-types is a Python library that enables the creation of 'phantom types' to enforce type safety at compile time without incurring runtime overhead. It leverages Python's `__instancecheck__` protocol and boolean predicates to allow developers to define stricter type constraints, helping to make 'illegal states unrepresentable' and mitigate 'shotgun parsing'. The library is currently at version 3.0.2 and follows semantic versioning after its 1.0 release, with new versions released periodically to add features, fix bugs, and maintain compatibility.
Common errors
-
phantom.errors.MissingDependency: To parse timezone aware/naive datetimes from strings, install 'python-dateutil' (e.g., pip install 'phantom-types[dateutil]').
cause Attempting to use `TZAware.parse()` or `TZNaive.parse()` without `python-dateutil` installed.fixInstall the `dateutil` extra: `pip install 'phantom-types[dateutil]'` or `pip install phantom-types[all]`. -
mypy: Argument 1 to "greet" has incompatible type "str"; expected "Name" [arg-type]
cause Passing a plain string literal or a string variable that has not been explicitly narrowed (e.g., via `Name.parse()` or `assert isinstance(my_var, Name)`) to a function expecting a phantom type.fixExplicitly convert the value using the phantom type's `parse` method (e.g., `greet(Name.parse(my_string))`) or assert its type at runtime to inform the static type checker (e.g., `assert isinstance(my_string_var, Name); greet(my_string_var)`). -
AttributeError: 'UTCDateTime' object has no attribute 'year'
cause When a phantom type is defined without inheriting from its concrete runtime base type (e.g., `datetime.datetime`), static type checkers might lose the knowledge of the underlying type's attributes after a type guard or parse operation.fixEnsure the phantom type inherits directly from its runtime type as the first base class. For instance, `class UTCDateTime(datetime.datetime, Phantom, predicate=is_utc): ...` instead of `class UTCDateTime(Phantom, predicate=is_utc): ...`.
Warnings
- breaking Prior to version 1.0, breaking changes could occur between minor versions. After 1.0, the library adheres to semantic versioning. Version 2.0.0 specifically dropped support for Python 3.7.
- gotcha Phantom types are implemented using a metaclass, which can lead to 'metaclass conflicts' when attempting to subclass a phantom type from another type that also uses a custom metaclass.
- gotcha Phantom types are incompatible with mutable data types. A type checker cannot detect if a mutation to an object changes it to no longer satisfy the phantom type's predicate, leading to silent inconsistencies.
- gotcha If a phantom type does not properly specify its runtime type bound (e.g., by not inheriting from the base type), static type checkers might 'erase' the runtime type information when performing type guarding, leading to `AttributeError` or other type-related issues.
Install
-
pip install phantom-types -
pip install phantom-types[all]
Imports
- Phantom
from phantom import Phantom
- contained
from phantom.predicates.collection import contained
- TZAware
from phantom.tz import TZAware
from phantom.datetime import TZAware
Quickstart
from phantom import Phantom
from phantom.predicates.collection import contained
from typing import TYPE_CHECKING
# Define a phantom type 'Name' that only accepts 'Jane' or 'Joe'
class Name(str, Phantom, predicate=contained({"Jane", "Joe"})):
pass
def greet(name: Name):
print(f"Hello {name}!")
# Valid usage: explicitly parsing a value into the phantom type
greet(Name.parse("Jane"))
# Valid usage: using runtime type checking (e.g., isinstance) to narrow the type
joe = "Joe"
assert isinstance(joe, Name) # This assertion informs type checkers
greet(joe)
# This call would typically be caught by a static type checker like MyPy
# as 'bird' does not satisfy the predicate for 'Name'.
# if TYPE_CHECKING:
# greet("bird") # E.g., Uncommenting this line would cause a MyPy error