Skip to main content

Phantom types for Python

Project description

phantom-types

Phantom types for Python.

Installation

python3 -m pip install phantom-types

Abstract

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), and high: float (defaults to positive infinity). Expects concrete subtypes to specify their runtime type bound as first base.
  • Open, (low, high)
    • Uses check=phantom.predicates.interval.open.
  • Closed, [low, high]
    • Uses check=phantom.predicates.interval.closed.
  • OpenClosed, (low, high]
    • Uses check=phantom.predicates.interval.open_closed.
  • ClosedOpen, [low, high)
    • Uses check=phantom.predicates.interval.closed_open.
Implemented intervals
  • Natural, (0, ∞)
  • NegativeInt, (-∞, 0)
  • Portion, (0, 1)

phantom.re

Takes pattern: Pattern[str] as class argument.

  • Match, uses phantom.predicates.re.is_match.
  • FullMatch, uses phantom.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 argument len: 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 returns True.
  • false: Predicate[object] always returns False.
  • negate(p: Predicate[T]) -> Predicate[T] negates a given predicate.
  • truthy: Predicate[object] returns True for truthy objects.
  • falsy: Predicate[object] returns True 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 contains value.
  • count(predicate: Predicate[int]) -> Predicate[Sized] creates a predicate that succeeds when the size of its argument satisfies the given predicate.

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 to a.
  • identical(a: object) -> Predicate[object] creates a new predicate that succeeds when its argument is identical to a.
  • of_type(t: Union[Type, Tuple[Type, ...]]) -> Predicate[object] creates a new predicate that succeeds when its argument is an instance of t.

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 than n.
  • le(n: float) -> Predicate[float] creates a new predicate that succeeds when its argument is less than or equal to n.
  • greater(n: float) -> Predicate[float] creates a new predicate that succeeds when its argument is strictly greater than n.
  • ge(n: float) -> Predicate[float] creates a new predicate that succeeds when its argument is greater than or equal to n.
  • 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 modulo n satisfies the given predicate p.
  • 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 given pattern.
  • is_full_match(pattern: Pattern[str]) -> Predicate[str] creates a new predicate that succeeds when its whole argument matches the given pattern.

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

phantom-types-0.3.0.tar.gz (16.5 kB view details)

Uploaded Source

Built Distribution

phantom_types-0.3.0-py3-none-any.whl (19.5 kB view details)

Uploaded Python 3

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

Hashes for phantom-types-0.3.0.tar.gz
Algorithm Hash digest
SHA256 e41bc3641520a2139bbbacc2a59d8f0a837aaf62d8ea466bea6b6c25b307da03
MD5 ca9299d4ae90ec958b53243a238e4e43
BLAKE2b-256 cde48d92fddb1fe547e3b39dbdbd6d6c57e1644b13bbe5e751586d493ee2b419

See more details on using hashes here.

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

Hashes for phantom_types-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9ee71bf1b1e16f6961430455141d7fe223b680e2acaebad26661d7adc309c0c5
MD5 98c31cbd8238c2cb08732bea37bef3d6
BLAKE2b-256 a32d7c866830b7fcc78ebca14cefdbdca78d1e13b9d8761e3b33157f8bf3b148

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page