Phantom types for Python will help you make illegal states unrepresentable and avoid shotgun parsing by enabling you to practice "Parse, don't validate".
$ python3 -m pip install phantom-types
There are a few extras available that can be used to either enable a feature or install a compatible version of a third-party library.
$ python3 -m pip install phantom-types[all]
By introducing a phantom type we can define a pre-condition for a function argument.
from phantom import Phantom from phantom.predicates.collection import contained class Name(str, Phantom, predicate=contained({"Jane", "Joe"})): ... def greet(name: Name): print(f"Hello {name}!")
Now this will be a valid call.
greet(Name.parse("Jane"))
... and so will this.
joe = "Joe" assert isinstance(joe, Name) greet(joe)
But this will yield a static type checking error.
To be clear, the reason the first example passes is not because the type checker somehow magically knows about our predicate, but because we provided the type checker with proof through the assert
. All the type checker cares about is that runtime cannot continue executing past the assertion, unless the variable is a Name
. If we move the calls around like in the example below, the type checker would give an error for the greet()
call.
joe = "Joe" greet(joe) assert isinstance(joe, Name)
By combining phantom types with a runtime type-checker like beartype or typeguard, we can achieve the same level of security as you'd gain from using contracts.
import datetime from beartype import beartype from phantom.datetime import TZAware @beartype def soon(dt: TZAware) -> TZAware: return dt + datetime.timedelta(seconds=10)
The soon
function will now validate that both its argument and return value is timezone aware, e.g. pre- and post conditions.
Phantom types are ready to use with pydantic and have integrated support out-of-the-box. Subclasses of Phantom
work with both pydantic's validation and its schema generation.
class Name(str, Phantom, predicate=contained({"Jane", "Joe"})): @classmethod def __schema__(cls) -> Schema: return super().__schema__() | { "description": "Either Jane or Joe", "format": "custom-name", } class Person(BaseModel): name: Name created: TZAware print(json.dumps(Person.schema(), indent=2))
The code above outputs the following JSONSchema.
{ "title": "Person", "type": "object", "properties": { "name": { "title": "Name", "description": "Either Jane or Joe", "format": "custom-name", "type": "string" }, "created": { "title": "TZAware", "description": "A date-time with timezone data.", "type": "string", "format": "date-time" } }, "required": ["name", "created"] }
Install development requirements, preferably in a virtualenv:
$ python3 -m pip install .[all,test,type-check]
Run tests:
$ pytest # or $ make test
Run type checker:
Linters and formatters are set up with goose, after installing it you can run it as:
# run all checks $ goose run --select=all # or just a single hook $ goose run mypy --select=all
In addition to static type checking, the project is set up with pytest-mypy-plugins to test that exposed mypy types work as expected, these checks will run together with the rest of the test suite, but you can single them out with the following command.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4