Phantom types for Python
Project description
phantom-types
Phantom types for Python.
Installation
python3 -m pip install phantom-types
Abstract
- Make illegal states unrepresentable.
- Parse, don't validate.
- Ghosts of Departed Proofs.
- Abuse
__instancecheck__
and type-guards. - Values are checked at runtime but no extra instances/subclasses are instantiated.
- Boolean predicate design is heavily inspired by fthomas/refined.
Usage
Shipped phantom types
phantom.boolean
Describes objects that coerce to either True
or False
when calling bool()
on them.
Truthy
Falsy
phantom.datetime
TZAware
TZNaive
phantom.interval
Describes narrower sets of numbers than int
and float
.
Base classes
Interval
- Takes class arguments
check: IntervalCheck
,low: float
(defaults to negative infinity), andhigh: float
(defaults to positive infinity). Expects concrete subtypes to specify their runtime type bound as first base.
- Takes class arguments
Open
,(low, high)
- Uses
check=phantom.predicates.interval.open
.
- Uses
Closed
,[low, high]
- Uses
check=phantom.predicates.interval.closed
.
- Uses
OpenClosed
,(low, high]
- Uses
check=phantom.predicates.interval.open_closed
.
- Uses
ClosedOpen
,[low, high)
- Uses
check=phantom.predicates.interval.closed_open
.
- Uses
Implemented intervals
Natural
,(0, ∞)
NegativeInt
,(-∞, 0)
Portion
,(0, 1)
phantom.re
Takes pattern: Pattern[str]
as class argument.
Match
, usesphantom.predicates.re.is_match
.FullMatch
, usesphantom.predicates.re.is_full_match
.
phantom.sized
Describes collections with size boundaries. These types should only be used with immutable collections. There is a naive check that eliminates some of the most common mutable collections in the instance check, however a guaranteed check is probably impossible to implement, so developer discipline is required.
PhantomSized[T]
, takes class argumentlen: Predicate[float]
.NonEmpty[T]
, a sized collection with at least one item.Empty[T]
, an empty collection.
Shipped predicates and factories
phantom.predicates.bool
true: Predicate[object]
always returnsTrue
.false: Predicate[object]
always returnsFalse
.negate(p: Predicate[T]) -> Predicate[T]
negates a given predicate.truthy: Predicate[object]
returnsTrue
for truthy objects.falsy: Predicate[object]
returnsTrue
for falsy objects.both(p: Predicate[T], q: Predicate[T]) -> Predicate[T]
creates a new predicate that succeeds when both of the given predicates succeed.all_of(predicates: Iterable[Predicate[T]]) -> Predicate[T]
creates a new predicate that succeeds when all of the given predicates succeed.any_of(predicates: Iterable[Predicate[T]] -> Predicate[T]
creates a new predicate that succeeds when at least one of the given predicates succeed.
phantom.predicates.collection
contains(value: object) -> Predicate[Container]
creates a new predicate that succeeds when its argument containsvalue
.count(predicate: Predicate[int]) -> Predicate[Sized]
creates a predicate that succeeds when the size of its argument satisfies the givenpredicate
.
phantom.predicates.datetime
is_tz_aware: Predicate[datetime.datetime]
succeeds if its argument is timezone aware.is_tz_naive: Predicate[datetime.datetime]
succeeds if its argument is timezone naive.
phantom.predicates.generic
equal(a: object) -> Predicate[object]
creates a new predicate that succeeds when its argument is equal toa
.identical(a: object) -> Predicate[object]
creates a new predicate that succeeds when its argument is identical toa
.of_type(t: Union[Type, Tuple[Type, ...]]) -> Predicate[object]
creates a new predicate that succeeds when its argument is an instance oft
.
phantom.predicates.interval
See corresponding shipped phantom types. Creates new predicates that succeed when their argument is strictly or non strictly between the upper and lower bounds.
open(low: float, high: float) -> Predicate[float]
open_closed(low: float, high: float) -> Predicate[float]
closed_open(low: float, high: float) -> Predicate[float]
closed(low: float, high: float) -> Predicate[float]
phantom.predicates.numeric
less(n: float) -> Predicate[float]
creates a new predicate that succeeds when its argument is strictly less thann
.le(n: float) -> Predicate[float]
creates a new predicate that succeeds when its argument is less than or equal ton
.greater(n: float) -> Predicate[float]
creates a new predicate that succeeds when its argument is strictly greater thann
.ge(n: float) -> Predicate[float]
creates a new predicate that succeeds when its argument is greater than or equal ton
.positive: Predicate[float]
succeeds when its argument is strictly greater than zero.non_positive: Predicate[float]
succeeds when its argument is less than or equal to zero.negative: Predicate[float]
succeeds when its argument is strictly less than zero.non_negative: Predicate[float]
succeeds when its argument is greater than or equal to zero.modulo(n: float, p: Predicate[float]) -> Predicate[float]
creates a new predicate that succeeds when its argument modulon
satisfies the given predicatep
.even: Predicate[int]
succeeds when its argument is even.odd: Predicate[int]
succeeds when its argument is odd.
phantom.predicates.re
is_match(pattern: Pattern[str]) -> Predicate[str]
creates a new predicate that succeeds when the start of its argument matches the givenpattern
.is_full_match(pattern: Pattern[str]) -> Predicate[str]
creates a new predicate that succeeds when its whole argument matches the givenpattern
.
Creating phantom types
Phantom types are created by subclassing phantom.base.Phantom
and defining an
__instancecheck__
method:
from typing import Any
from typing import TYPE_CHECKING
from phantom.base import Phantom
class Greeting(Phantom):
@classmethod
def __instancecheck__(cls, instance: Any) -> bool:
return (
isinstance(instance, str)
and instance.startswith(("Hello", "Hi"))
)
hello = "Hello there"
# We can narrow types using mypy's type guards
assert isinstance(hello, Greeting)
# or explicitly when we need to
hi = Greeting.from_instance("Hi there")
# The runtime types are unchanged and will still be str for our greetings
assert type(hello) is str
assert type(hi) is str
# But their static types will be Greeting, retaining the information that our
# strings are not just any strs
if TYPE_CHECKING:
reveal_type(hello)
reveal_type(hi)
# As this string doesn't fulfill our __instancecheck__, it will not be an
# instance of Greeting.
assert not isinstance("Goodbye", Greeting)
Checkout out the dacite example for how to create dataclasses with rich phantom-typed fields without duplicating type definitions or losing parsed information.
Using predicates
Most of the shipped phantom types are implemented using boolean predicates. A boolean
predicate is simply a function that takes a single argument and returns either True
or False
. While using boolean predicates is not necessary to use phantom types,
building up a library of types doing so allows reusing small and easily testable
functions to create a plethora of specialized types. Boolean predicates are usually easy
to reason about as they are pure functions with only two possible return values.
Studying the phantom types shipped in this library is recommended for gaining deeper insight into how to implement more complicated types.
Now, looking at the example we implemented by subclassing Phantom
and providing
an __instancecheck__
method, let's try and achieve the same using predicates. The
PredicateType
class already implements an __instancecheck__
method and will usually
reduce the amount of boilerplate required.
from phantom.base import PredicateType
# A boolean predicate that checks if a given string is a greeting. This function is of
# type `Predicate[str]` as it requires its argument to be a `str`.
def is_greeting(instance: str) -> bool:
return instance.startswith(("Hello", "Hi"))
# Since our predicate requires its argument to be a `str`, we must make the bound of the
# phantom type `str` as well.
class Greeting(PredicateType, bound=str, predicate=is_greeting):
...
# Now we can make the same operations as with our previous example.
hello = "Hello there"
assert isinstance(hello, Greeting)
hi = Greeting.from_instance("Hi there")
As you can see, in addition to having less boilerplate than the previous example, this style also has the added benefit of separating out business logic into simple reusable functions.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file phantom-types-0.3.0.tar.gz
.
File metadata
- Download URL: phantom-types-0.3.0.tar.gz
- Upload date:
- Size: 16.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.2.0 pkginfo/1.5.0.1 requests/2.24.0 setuptools/50.3.0 requests-toolbelt/0.9.1 tqdm/4.50.0 CPython/3.8.2
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | e41bc3641520a2139bbbacc2a59d8f0a837aaf62d8ea466bea6b6c25b307da03 |
|
MD5 | ca9299d4ae90ec958b53243a238e4e43 |
|
BLAKE2b-256 | cde48d92fddb1fe547e3b39dbdbd6d6c57e1644b13bbe5e751586d493ee2b419 |
File details
Details for the file phantom_types-0.3.0-py3-none-any.whl
.
File metadata
- Download URL: phantom_types-0.3.0-py3-none-any.whl
- Upload date:
- Size: 19.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.2.0 pkginfo/1.5.0.1 requests/2.24.0 setuptools/50.3.0 requests-toolbelt/0.9.1 tqdm/4.50.0 CPython/3.8.2
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 9ee71bf1b1e16f6961430455141d7fe223b680e2acaebad26661d7adc309c0c5 |
|
MD5 | 98c31cbd8238c2cb08732bea37bef3d6 |
|
BLAKE2b-256 | a32d7c866830b7fcc78ebca14cefdbdca78d1e13b9d8761e3b33157f8bf3b148 |