newertype


Namenewertype JSON
Version 0.4.0 PyPI version JSON
download
home_pageNone
SummaryAn Implementation of the NewType Pattern for Python that works in dynamic contexts.
upload_time2025-10-26 16:35:28
maintainerNone
docs_urlNone
authorNone
requires_python>=3.8
licenseNone
keywords newtype python runtime type-safety typechecker typehints typing
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # newertype

An Implementation of the NewType Pattern for Python that works in dynamic contexts.

  [![PyPI version](https://img.shields.io/pypi/v/newertype.svg)](https://pypi.org/project/newertype/)
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
  [![Python Versions](https://img.shields.io/pypi/pyversions/newertype.svg)](https://pypi.org/project/newertype/)
  [![Downloads](https://pepy.tech/badge/newertype)](https://pepy.tech/project/newertype)
  [![Python package](https://github.com/evanjpw/newertype/actions/workflows/python-package.yml/badge.svg)](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  [![PyPI version](https://img.shields.io/pypi/v/newertype.svg)](https://pypi.org/project/newertype/)\n  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n  [![Python Versions](https://img.shields.io/pypi/pyversions/newertype.svg)](https://pypi.org/project/newertype/)\n  [![Downloads](https://pepy.tech/badge/newertype)](https://pepy.tech/project/newertype)\n  [![Python package](https://github.com/evanjpw/newertype/actions/workflows/python-package.yml/badge.svg)](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"
}
        
Elapsed time: 4.82896s