# newertype
An Implementation of the NewType Pattern for Python that works in dynamic contexts.
[](https://pypi.org/project/newertype/)
[](https://opensource.org/licenses/MIT)
[](https://pypi.org/project/newertype/)
[](https://pepy.tech/project/newertype)
[](https://github.com/evanjpw/newertype/actions/workflows/python-package.yml)
> [!WARNING]
> **Breaking Change** in coming 1.0.0
> (for a really weird edge case that almost certainly doesn't apply to you).
> See [the documentation](https://evanjpw.github.io/newertype/) for more information.
## What is it?
`NewerType` is a package that provides a semi-transparent wrapper to an existing type that allows it to be used
mostly as if it's just the wrapped type, but which allows type checking as if it's a distinct type at runtime.
With the addition to Python of [PEP 483](https://peps.python.org/pep-0483/),
[PEP 484](https://peps.python.org/pep-0484/), & the
[typing](https://docs.python.org/3/library/typing.html#module-typing) package, Python added support for type
hints. That included an implementation of the Haskell [`newtype`](https://wiki.haskell.org/Newtype) which was
cleverly called `NewType`.
As explained in [the documentation](https://docs.python.org/3/library/typing.html#typing.NewType),
Python's `NewType` is, like most of the
typing library, meant for use by static type checkers. This means that, when the code is running, the _Newness_ of
the type is erased, leaving just the wrapped type & no way to tell that there was ever a `Newtype`, either by
the code or by Python itself.
`NewerType` provides the same kind of wrapper as `NewType`, but allows (& enforces) type checking at runtime.
this means, for example, that if you wrap an `int` in a `NewerType`, you can do all of the arithmetic &
comparison operations on an instance of that type that you could with a normal `int` with either different
instances of that type, or `int`s. But you will not be able to mix _different_ `NewerType`s, even if they
all wrap `int`s.
This allows you to never have to worry if you are adding `Miles` to `Kilometers`, or mixing up a `UserName`
with a `Password`.
### Main Features
* A wrapper that allows dynamic type checking while mostly not getting in the way
* Carries type information with the object so you can always use `isinstace()` or `type()` to know what it is
* Forwards the magic methods from the wrapped object so things like arithmetic or indexing work
* Allows you to customize what methods are forwarded
* No dependencies!
## Installation
Current stable version:
```shell
pip install newertype
```
Newest thing on GitHub:
```shell
pip install git+https://github.com/evanjpw/newertype.git
```
## Usage
Basic usage:
```python
from newertype import NewerType
AType = NewerType("AType", int) # `AType` is a new type that wraps an int
a_type = AType(14) # Make an instance of this new type
isinstance(a_type, AType) # `a_type` is an `AType`
# Returns: True
isinstance(a_type, int) # `a_type` is _NOT_ an `int`
# Returns: False
str(a_type.__class__.__name__) == "AType"
# Returns: True
```
You can use the new type as if it's the wrapped type:
```python
AType = NewerType("AType", int) # Let's make some types!
a_type_1 = AType(7)
a_type_2 = AType(7) # Two different instances with the same class
a_type_1 == a_type_2 # You can compare them as if they were just `int`s
# Returns: True
EType = NewerType("EType", int)
e_type_1 = EType(7)
e_type_2 = EType(14)
e_type_2 > e_type_1 # All of the `int` operations work
# Returns: True
a_type_1 == e_type_1 # But different types are not equal, even if the wrapped value is
Returns: False
IType = NewerType("IType", int)
JType = NewerType("JType", int)
i_type_1 = IType(7)
i_type_2 = IType(14)
i_type_1 + i_type_2 # Arithmetic works!
# Returns: 21
j_type_1 = JType(7)
i_type_1 + j_type_1 # But not if you try to mix `NewerType`s
# "TypeError: unsupported operand type(s) for +: 'IType' and 'JType'"
int(i_type_1) < int(i_type_2) # Conversions that work for the inner type work also
# Returns: True
```
Accessing the wrapped data directly:
```python
a_type = AType(14)
a_type.inner # the `inner` property gets the contained value
# Returns: 14
a_type.inner = 27 # `inner` can also be used to modify the value
a_type.inner
# Returns: 27
```
The "truthiness" & string representations are sensible:
```python
SType = NewerType("SType", float)
s_type = SType(2.71828182845904523536028747135266249775724709369995)
str(s_type)
# Returns: "SType(2.718281828459045)"
repr(s_type)
# Returns: "SType(2.718281828459045)"
bool(s_type)
# Returns: True
bytes(s_type) # `bytes()` only works if it works with the wrapped type
# "TypeError: cannot convert 'float' object to bytes"
s_type.inner = 0.0
bool(s_type)
# Returns: False
```
What about forwarding your own methods on your own classes? NewerType can handle that:
```python
# First, define a class. It can have the standard indexing methods, but also some unique ones:
class Forwardable(UserDict):
def forwarded(self, value):
return value
def also_forwarded(self, key):
return self[key]
def __getitem__(self, item):
return super().__getitem__(item)
def __setitem__(self, key, value):
super().__setitem__(key, value)
# The normal behavior is for NewerType to forward the standard methods but ignore the custom ones:
FO1Type = NewerType("FO1Type", Forwardable)
fo1_type_1 = FO1Type(Forwardable())
fo1_type_1["a"] = 5 # `__setitem__` is a standard method, so it's forwarded
fo1_type_1["a"] # So is `__getitem__`
# Returns: 5
fo1_type_1.forwarded(5) # But unique methods are not forwarded
# "AttributeError: FO1Type' object has no attribute 'forwarded'"
# We can use "extra_forwards" to specify the additional methods we'd like to forward:
FO2Type = NewerType(
"FO2Type", Forwardable, extra_forwards=["forwarded", "also_forwarded"]
)
fo2_type_1 = FO2Type(Forwardable())
fo2_type_1["e"] = 7 # This continues to work
fo2_type_1["e"] # As does this
# Returns: 7
fo2_type_1.also_forwarded("e") # But now this works also!
# Returns: 7
# But what if we _don't_ want to forward the standard methods? Use "no_def_forwards":
FO3Type = NewerType(
"FO3Type", Forwardable, extra_forwards=["also_forwarded"], no_def_forwards=True
)
fo3_type_1 = FO3Type(Forwardable())
fo3_type_1.inner["g"] = 8
fo3_type_1.also_forwarded("g") # The extra methods continue to work
# Returns: 8
fo3_type_1["g"] # But the standard ones don't (unless we specifically mention them in "extra_forwards")
# "TypeError: 'FO3Type' object is not subscriptable"
```
# Documentation
See [the documentation](https://evanjpw.github.io/newertype/)
## TBD
* The `bytes()` built-in currently just forces all wrapped `str` objects to "utf-8" as an encoding.
If you need a *different* encoding, use `bytes()` of `.inner`.
* There are a *bunch* more methods that should be in the whitelist for forwarding. That's a work in progress.
* Pickling _does not work_, currently, but can be made to work if there is popular demand.
## Project Resources
* Documentation - TBD
* [Issue tracker](https://github.com/evanjpw/newertype/issues)
* [Source code](https://github.com/evanjpw/newertype)
* [Change log](https://github.com/evanjpw/newertype/blob/main/CHANGELOG.md)
## License
Licensed under the [MIT LICENSE](https://www.mit.edu/~amini/LICENSE.md)
Raw data
{
"_id": null,
"home_page": null,
"name": "newertype",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": null,
"keywords": "newtype, python, runtime, type-safety, typechecker, typehints, typing",
"author": null,
"author_email": "Evan Williams <evanjpw@gmail.com>",
"download_url": "https://files.pythonhosted.org/packages/4d/07/0c69f13aaf1a95367aff32ad77eb7c861978a153e9eb67e10f72b269fbaa/newertype-0.4.0.tar.gz",
"platform": null,
"description": "# newertype\n\nAn Implementation of the NewType Pattern for Python that works in dynamic contexts.\n\n [](https://pypi.org/project/newertype/)\n [](https://opensource.org/licenses/MIT)\n [](https://pypi.org/project/newertype/)\n [](https://pepy.tech/project/newertype)\n [](https://github.com/evanjpw/newertype/actions/workflows/python-package.yml)\n\n> [!WARNING]\n> **Breaking Change** in coming 1.0.0\n> (for a really weird edge case that almost certainly doesn't apply to you).\n> See [the documentation](https://evanjpw.github.io/newertype/) for more information.\n\n## What is it?\n\n`NewerType` is a package that provides a semi-transparent wrapper to an existing type that allows it to be used\nmostly as if it's just the wrapped type, but which allows type checking as if it's a distinct type at runtime.\n\nWith the addition to Python of [PEP 483](https://peps.python.org/pep-0483/),\n[PEP 484](https://peps.python.org/pep-0484/), & the\n[typing](https://docs.python.org/3/library/typing.html#module-typing) package, Python added support for type\nhints. That included an implementation of the Haskell [`newtype`](https://wiki.haskell.org/Newtype) which was\ncleverly called `NewType`.\nAs explained in [the documentation](https://docs.python.org/3/library/typing.html#typing.NewType),\nPython's `NewType` is, like most of the\ntyping library, meant for use by static type checkers. This means that, when the code is running, the _Newness_ of\nthe type is erased, leaving just the wrapped type & no way to tell that there was ever a `Newtype`, either by\nthe code or by Python itself.\n\n`NewerType` provides the same kind of wrapper as `NewType`, but allows (& enforces) type checking at runtime.\nthis means, for example, that if you wrap an `int` in a `NewerType`, you can do all of the arithmetic &\ncomparison operations on an instance of that type that you could with a normal `int` with either different\ninstances of that type, or `int`s. But you will not be able to mix _different_ `NewerType`s, even if they\nall wrap `int`s.\n\nThis allows you to never have to worry if you are adding `Miles` to `Kilometers`, or mixing up a `UserName`\nwith a `Password`.\n\n### Main Features\n\n* A wrapper that allows dynamic type checking while mostly not getting in the way\n* Carries type information with the object so you can always use `isinstace()` or `type()` to know what it is\n* Forwards the magic methods from the wrapped object so things like arithmetic or indexing work\n* Allows you to customize what methods are forwarded\n* No dependencies!\n\n## Installation\n\nCurrent stable version:\n```shell\npip install newertype\n```\n\nNewest thing on GitHub:\n```shell\npip install git+https://github.com/evanjpw/newertype.git\n```\n\n## Usage\n\nBasic usage:\n\n```python\nfrom newertype import NewerType\n\nAType = NewerType(\"AType\", int) # `AType` is a new type that wraps an int\na_type = AType(14) # Make an instance of this new type\nisinstance(a_type, AType) # `a_type` is an `AType`\n# Returns: True\nisinstance(a_type, int) # `a_type` is _NOT_ an `int`\n# Returns: False\nstr(a_type.__class__.__name__) == \"AType\"\n# Returns: True\n```\n\nYou can use the new type as if it's the wrapped type:\n\n```python\nAType = NewerType(\"AType\", int) # Let's make some types!\na_type_1 = AType(7)\na_type_2 = AType(7) # Two different instances with the same class\na_type_1 == a_type_2 # You can compare them as if they were just `int`s\n# Returns: True\n\nEType = NewerType(\"EType\", int)\ne_type_1 = EType(7)\ne_type_2 = EType(14)\ne_type_2 > e_type_1 # All of the `int` operations work\n# Returns: True\na_type_1 == e_type_1 # But different types are not equal, even if the wrapped value is\nReturns: False\n\nIType = NewerType(\"IType\", int)\nJType = NewerType(\"JType\", int)\ni_type_1 = IType(7)\ni_type_2 = IType(14)\ni_type_1 + i_type_2 # Arithmetic works!\n# Returns: 21\n\nj_type_1 = JType(7)\ni_type_1 + j_type_1 # But not if you try to mix `NewerType`s\n# \"TypeError: unsupported operand type(s) for +: 'IType' and 'JType'\"\nint(i_type_1) < int(i_type_2) # Conversions that work for the inner type work also\n# Returns: True\n```\n\nAccessing the wrapped data directly:\n\n```python\na_type = AType(14)\na_type.inner # the `inner` property gets the contained value\n# Returns: 14\na_type.inner = 27 # `inner` can also be used to modify the value\na_type.inner\n# Returns: 27\n```\n\nThe \"truthiness\" & string representations are sensible:\n\n```python\nSType = NewerType(\"SType\", float)\ns_type = SType(2.71828182845904523536028747135266249775724709369995)\nstr(s_type)\n# Returns: \"SType(2.718281828459045)\"\nrepr(s_type)\n# Returns: \"SType(2.718281828459045)\"\nbool(s_type)\n# Returns: True\nbytes(s_type) # `bytes()` only works if it works with the wrapped type\n# \"TypeError: cannot convert 'float' object to bytes\"\n\ns_type.inner = 0.0\nbool(s_type)\n# Returns: False\n```\n\nWhat about forwarding your own methods on your own classes? NewerType can handle that:\n\n```python\n# First, define a class. It can have the standard indexing methods, but also some unique ones:\nclass Forwardable(UserDict):\n def forwarded(self, value):\n return value\n\n def also_forwarded(self, key):\n return self[key]\n\n def __getitem__(self, item):\n return super().__getitem__(item)\n\n def __setitem__(self, key, value):\n super().__setitem__(key, value)\n\n# The normal behavior is for NewerType to forward the standard methods but ignore the custom ones:\nFO1Type = NewerType(\"FO1Type\", Forwardable)\nfo1_type_1 = FO1Type(Forwardable())\nfo1_type_1[\"a\"] = 5 # `__setitem__` is a standard method, so it's forwarded\nfo1_type_1[\"a\"] # So is `__getitem__`\n# Returns: 5\nfo1_type_1.forwarded(5) # But unique methods are not forwarded\n# \"AttributeError: FO1Type' object has no attribute 'forwarded'\"\n\n# We can use \"extra_forwards\" to specify the additional methods we'd like to forward:\nFO2Type = NewerType(\n \"FO2Type\", Forwardable, extra_forwards=[\"forwarded\", \"also_forwarded\"]\n)\nfo2_type_1 = FO2Type(Forwardable())\nfo2_type_1[\"e\"] = 7 # This continues to work\nfo2_type_1[\"e\"] # As does this\n# Returns: 7\nfo2_type_1.also_forwarded(\"e\") # But now this works also!\n# Returns: 7\n\n# But what if we _don't_ want to forward the standard methods? Use \"no_def_forwards\":\nFO3Type = NewerType(\n \"FO3Type\", Forwardable, extra_forwards=[\"also_forwarded\"], no_def_forwards=True\n)\nfo3_type_1 = FO3Type(Forwardable())\nfo3_type_1.inner[\"g\"] = 8\nfo3_type_1.also_forwarded(\"g\") # The extra methods continue to work\n# Returns: 8\nfo3_type_1[\"g\"] # But the standard ones don't (unless we specifically mention them in \"extra_forwards\")\n# \"TypeError: 'FO3Type' object is not subscriptable\"\n```\n\n# Documentation\n\nSee [the documentation](https://evanjpw.github.io/newertype/)\n\n## TBD\n\n* The `bytes()` built-in currently just forces all wrapped `str` objects to \"utf-8\" as an encoding.\n If you need a *different* encoding, use `bytes()` of `.inner`.\n* There are a *bunch* more methods that should be in the whitelist for forwarding. That's a work in progress.\n* Pickling _does not work_, currently, but can be made to work if there is popular demand.\n\n## Project Resources\n\n* Documentation - TBD\n* [Issue tracker](https://github.com/evanjpw/newertype/issues)\n* [Source code](https://github.com/evanjpw/newertype)\n* [Change log](https://github.com/evanjpw/newertype/blob/main/CHANGELOG.md)\n\n## License\n\nLicensed under the [MIT LICENSE](https://www.mit.edu/~amini/LICENSE.md)\n",
"bugtrack_url": null,
"license": null,
"summary": "An Implementation of the NewType Pattern for Python that works in dynamic contexts.",
"version": "0.4.0",
"project_urls": {
"changelog": "https://github.com/evanjpw/newertype/blob/main/CHANGELOG.md",
"documentation": "https://evanjpw.github.io/newertype/",
"homepage": "https://github.com/evanjpw/newertype",
"issues": "https://github.com/evanjpw/newertype/issues",
"repository": "https://github.com/evanjpw/newertype"
},
"split_keywords": [
"newtype",
" python",
" runtime",
" type-safety",
" typechecker",
" typehints",
" typing"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "00d9d44db83b8cbd86dbb8143d2397c4b581894f8a665913133b051fb1f0aacd",
"md5": "3bf803be3ba5fa2c996ac3aed4a2061c",
"sha256": "99dcadb1db01f214e3a45e2261a59c4dfac9a1e14543f8383e4ccb340e894855"
},
"downloads": -1,
"filename": "newertype-0.4.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "3bf803be3ba5fa2c996ac3aed4a2061c",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8",
"size": 8389,
"upload_time": "2025-10-26T16:35:27",
"upload_time_iso_8601": "2025-10-26T16:35:27.077129Z",
"url": "https://files.pythonhosted.org/packages/00/d9/d44db83b8cbd86dbb8143d2397c4b581894f8a665913133b051fb1f0aacd/newertype-0.4.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "4d070c69f13aaf1a95367aff32ad77eb7c861978a153e9eb67e10f72b269fbaa",
"md5": "36b1e3f40ceb0d8f232c51e10fb9a7f4",
"sha256": "9c0a92a5e2114667a7257af4ee90810389b77e72dfab58e5c818803c9cd02d39"
},
"downloads": -1,
"filename": "newertype-0.4.0.tar.gz",
"has_sig": false,
"md5_digest": "36b1e3f40ceb0d8f232c51e10fb9a7f4",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 104905,
"upload_time": "2025-10-26T16:35:28",
"upload_time_iso_8601": "2025-10-26T16:35:28.575054Z",
"url": "https://files.pythonhosted.org/packages/4d/07/0c69f13aaf1a95367aff32ad77eb7c861978a153e9eb67e10f72b269fbaa/newertype-0.4.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-10-26 16:35:28",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "evanjpw",
"github_project": "newertype",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "newertype"
}