python-newtype Documentation¶
Welcome to the python-newtype documentation! This library implements a powerful type system feature from type theory, allowing you to create true subtypes that maintain type consistency throughout method calls.
Overview¶
python-newtype is unique in its implementation of the "new type" concept from type theory. The key feature is its ability to maintain subtype relationships through method calls and provide robust type validation. Here's a comprehensive example that showcases its main features:
from newtype import NewType, newtype_exclude
import pytest
import re
class EmailStr(NewType(str)):
def __init__(self, value: str):
super().__init__(value)
if '@' not in value:
raise TypeError("`EmailStr` requires a '@' symbol within")
self._local_part, self._domain_part = value.split('@')
@newtype_exclude
def __str__(self):
return f"<Email - Local Part: {self.local_part}; Domain Part: {self.domain_part}>"
@property
def local_part(self):
"""Return the local part of the email address."""
return self._local_part
@property
def domain_part(self):
"""Return the domain part of the email address."""
return self._domain_part
@property
def full_email(self):
"""Return the full email address."""
return str(self)
@classmethod
def from_string(cls, email: str):
"""Create an EmailStr instance from a string."""
return cls(email)
@staticmethod
def is_valid_email(email: str) -> bool:
"""Check if the provided string is a valid email format."""
email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(email_regex, email) is not None
def test_emailstr_replace():
"""`EmailStr` uses `str.replace(..)` as its own method, returning an instance of `EmailStr`
if the resultant `str` instance is a value `EmailStr`."""
peter_email = EmailStr("peter@gmail.com")
smith_email = EmailStr("smith@gmail.com")
with pytest.raises(Exception):
# this raises because `peter_email` is no longer an instance of `EmailStr`
peter_email = peter_email.replace("peter@gmail.com", "petergmail.com")
# this works because the entire email can be 'replaced'
james_email = smith_email.replace("smith@gmail.com", "james@gmail.com")
# comparison with `str` is built-in
assert james_email == "james@gmail.com"
# `james_email` is still an `EmailStr`
assert isinstance(james_email, EmailStr)
# this works because the local part can be 'replaced'
jane_email = james_email.replace("james", "jane")
# `jane_email` is still an `EmailStr`
assert isinstance(jane_email, EmailStr)
assert jane_email == "jane@gmail.com"
def test_emailstr_properties_methods():
"""Test the property, class method, and static method of EmailStr."""
# Test property
email = EmailStr("test@example.com")
# `property` is not coerced to `EmailStr`
assert email.full_email == "<Email - Local Part: test; Domain Part: example.com>"
assert isinstance(email.full_email, str)
# `property` is not coerced to `EmailStr`
assert not isinstance(email.full_email, EmailStr)
assert email.local_part == "test"
assert email.domain_part == "example.com"
# Test class method
email_from_string = EmailStr.from_string("classmethod@example.com")
# `property` is not coerced to `EmailStr`
assert email_from_string.full_email == "<Email - Local Part: classmethod; Domain Part: example.com>"
assert email_from_string.local_part == "classmethod"
assert email_from_string.domain_part == "example.com"
# Test static method
assert EmailStr.is_valid_email("valid.email@example.com") is True
assert EmailStr.is_valid_email("invalid-email.com") is False
Key Features¶
1. True Subtype Preservation¶
- When a supertype method returns a value, python-newtype automatically instantiates a value of the subtype
- Type consistency is maintained throughout the entire chain of method calls
- All inherited methods maintain proper return types
2. Robust Type Validation¶
- Custom initialization with validation logic
- Support for strict and non-strict validation modes
- Proper inheritance of validation rules
3. Property and Method Support¶
- Full support for Python's property decorators
- Cached properties work seamlessly
- Static methods and class methods are preserved and are not modified
4. Type Safety and Inheritance¶
- Operations on subtypes never "leak" back to supertypes
- Behavioral subtyping with invariant maintenance
- Liskov Substitution Principle compliance
More Examples¶
Method Exclusion¶
from newtype import NewType, newtype_exclude
class SafeStr(NewType(str)):
def __init__(self, value: str):
if "<script>" in value.lower():
raise ValueError("XSS attempt detected")
super().__init__()
# This method will return a regular str, not a SafeStr
@newtype_exclude
def unsafe_operation(self):
return str(self)
# This method will return a SafeStr
def safe_operation(self):
return self.lower()
# Usage
text = SafeStr("Hello")
assert isinstance(text.unsafe_operation(), str) # Regular str
assert isinstance(text.safe_operation(), SafeStr) # SafeStr
Advanced Validation¶
from typing import Union, Optional
class IntegerStr(NewType(str)):
def __init__(self, value: Union[str, int], base: int = 10):
if isinstance(value, int):
value = str(value)
try:
int(value, base)
except ValueError:
raise ValueError(f"Value must be a valid integer in base {base}")
super().__init__(value)
def to_int(self, base: Optional[int] = None) -> int:
return int(self, base or 10)
# Handle different number bases
num = IntegerStr("42") # Decimal
hex_num = IntegerStr("2A", 16) # Hexadecimal
print(hex_num.to_int(16)) # 42
Getting Started¶
Check out our Quick Start Guide to begin using python-newtype in your projects.
Installation¶
See the Installation Guide for detailed installation instructions.
Contributing¶
We welcome contributions! Please see our Contributing Guide for details on how to get involved.