Package: phantom-types
phantom-typesPhantom typesfor Python will help you make illegal states unrepresentable and
avoid shotgun parsing by enabling you to practice"Parse, don't validate".Checkout the complete documentation on Read the Docs →Installation$python3-mpipinstallphantom-typesExtrasThere are a few extras available that can be used to either enable a feature or install
a compatible version of a third-party library.Extra nameFeature[dateutil]Installspython-dateutil. Required for parsing strings withTZAwareandTZNaive.[phonenumbers]Installsphonenumbers. Required to usephantom.ext.phonenumbers.[pydantic]Installspydantic.[hypothesis]Installshypothesis.[all]Installs all of the above.$python3-mpipinstallphantom-types[all]ExamplesBy introducing a phantom type we can define a pre-condition for a function argument.fromphantomimportPhantomfromphantom.predicates.collectionimportcontainedclassName(str,Phantom,predicate=contained({"Jane","Joe"})):...defgreet(name:Name):print(f"Hello{name}!")Now this will be a valid call.greet(Name.parse("Jane"))... and so will this.joe="Joe"assertisinstance(joe,Name)greet(joe)But this will yield a static type checking error.greet("bird")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 theassert. All the type checker cares about is that runtime cannot continue
executing past the assertion, unless the variable is aName. If we move the calls
around like in the example below, the type checker would give an error for thegreet()call.joe="Joe"greet(joe)assertisinstance(joe,Name)Runtime type checkingBy combining phantom types with a runtime type-checker likebeartypeortypeguard,
we can achieve the same level of security as you'd gain from usingcontracts.importdatetimefrombeartypeimportbeartypefromphantom.datetimeimportTZAware@beartypedefsoon(dt:TZAware)->TZAware:returndt+datetime.timedelta(seconds=10)Thesoonfunction will now validate that both its argument and return value is
timezone aware, e.g. pre- and post conditions.Pydantic supportPhantom types are ready to use withpydanticand haveintegrated
supportout-of-the-box. Subclasses ofPhantomwork with both
pydantic's validation and its schema generation.className(str,Phantom,predicate=contained({"Jane","Joe"})):@classmethoddef__schema__(cls)->Schema:returnsuper().__schema__()|{"description":"Either Jane or Joe","format":"custom-name",}classPerson(BaseModel):name:Namecreated:TZAwareprint(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"]}DevelopmentInstall development requirements, preferably in a virtualenv:$python3-mpipinstall.[all,test]Run tests:$pytest# or$maketestLinting and static type checking is setup withpre-commit, after installing it you can
setup hooks with the following command, so that checks run before you push changes.# configure hooks to run when pushing$pre-commitinstall-tpre-push# or when committing$pre-commitinstall-tpre-commit# run all checks$pre-commitrun--all-files# or just a single hook$pre-commitrunmypy--all-filesIn addition to static type checking, the project is setup withpytest-mypy-pluginsto
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.$maketest-typing