Type Composition & Inheritance
pytypeinput supports powerful type composition, allowing you to build complex types from simpler ones while preserving all constraints and metadata.
Basic Composition
Build reusable type aliases with constraints:
from typing import Annotated, TypeAlias
from pydantic import Field
from pytypeinput import analyze_type
# Base constraint
PositiveInt: TypeAlias = Annotated[int, Field(ge=0)]
# Extend with additional constraint
Percentage: TypeAlias = Annotated[PositiveInt, Field(le=100)]
# All constraints are preserved
param = analyze_type(Percentage, name="completion")
print(param.constraints.metadata) # [ge=0, le=100]
HTML Renderer Demo:
How Constraint Merging Works
When combining multiple Field() constraints:
Different Constraint Types → Merged
Base: TypeAlias = Annotated[str, Field(max_length=100)]
Enhanced: TypeAlias = Annotated[Base, Field(min_length=5)]
# Result: min_length=5 AND max_length=100 ✅
Same Constraint Type → Last Wins
Loose: TypeAlias = Annotated[str, Field(max_length=100)]
Strict: TypeAlias = Annotated[Loose, Field(max_length=50)]
# Result: max_length=50 (100 is discarded)
Recognized constraints: pattern, min_length, max_length, ge, le, gt, lt
String Composition
# Base types
RequiredString: TypeAlias = Annotated[str, Field(min_length=1)]
ShortString: TypeAlias = Annotated[str, Field(max_length=50)]
# Combine
ShortRequiredString: TypeAlias = Annotated[RequiredString, Field(max_length=50)]
# Result: min_length=1 AND max_length=50 ✅
# Override
VeryShortString: TypeAlias = Annotated[ShortString, Field(max_length=20)]
# Result: max_length=20 (last wins)
# Pattern + constraints
Username: TypeAlias = Annotated[
str,
Field(min_length=3, max_length=20, pattern=r'^[a-zA-Z0-9_]+$')
]
Adding UI Metadata
from pytypeinput import Slider, Label, Description
# Add UI to constrained type
PercentageSlider: TypeAlias = Annotated[
Percentage, # Has ge=0, le=100
Slider(),
Label("Volume")
]
# UI metadata is always accumulated
Base: TypeAlias = Annotated[int, Label("Value")]
Enhanced: TypeAlias = Annotated[Base, Description("Enter a value")]
# Has BOTH label and description ✅
Special Type Inheritance
Extend special types with additional constraints:
from pytypeinput import Email
# ✅ Email with length constraints
ValidEmail: TypeAlias = Annotated[Email, Field(max_length=254)]
StrictEmail: TypeAlias = Annotated[Email, Field(min_length=5, max_length=100)]
param = analyze_type(ValidEmail, name="email")
print(param.widget_type) # "Email" ✅
# Has: Email pattern + max_length=254
⚠️ Pattern Override Warning
When you override a pattern, the widget type is determined by the last pattern:
from pytypeinput import Email
ValidEmail: TypeAlias = Annotated[Email, Field(max_length=254)]
WorkEmail: TypeAlias = Annotated[ValidEmail, Field(pattern=r'.*@company\.com$')]
param = analyze_type(WorkEmail, name="work_email")
print(param.widget_type) # None ⚠️ (not "Email"!)
# Pattern: '.*@company\.com$' (replaces Email pattern)
# max_length: 254 (preserved)
Why: The "last wins" rule applies to all constraints. The custom pattern replaces the Email pattern and isn't recognized as a special type.
Solution:
# ✅ Create a combined pattern
CompanyEmailPattern = r'^[a-zA-Z0-9._%+-]+@company\.com$'
WorkEmail: TypeAlias = Annotated[
str,
Field(pattern=CompanyEmailPattern, max_length=254)
]
# ✅ Or use two-layer validation
email: Email # UI widget
# Validate domain separately in business logic
Multi-Level Composition
Level1: TypeAlias = Annotated[int, Field(ge=0)]
Level2: TypeAlias = Annotated[Level1, Field(le=100)]
Level3: TypeAlias = Annotated[Level2, Slider()]
Level4: TypeAlias = Annotated[Level3, Label("Brightness")]
# Result: ge=0, le=100, is_slider=True, label="Brightness" ✅
With overrides:
Base: TypeAlias = Annotated[int, Field(ge=0, le=100)]
Strict: TypeAlias = Annotated[Base, Field(ge=10)] # Override ge
# Result: ge=10 (not 0!), le=100 ✅
Real-World Example
from typing import Annotated, TypeAlias
from dataclasses import dataclass
from pydantic import Field
from pytypeinput import Email, analyze_dataclass, Label, Description, Slider
# Build your type library
PositiveInt: TypeAlias = Annotated[int, Field(ge=0)]
Percentage: TypeAlias = Annotated[PositiveInt, Field(le=100)]
PercentageSlider: TypeAlias = Annotated[Percentage, Slider()]
Username: TypeAlias = Annotated[
str,
Field(min_length=3, max_length=20, pattern=r'^[a-zA-Z0-9_]+$'),
Label("Username")
]
ValidEmail: TypeAlias = Annotated[
Email,
Field(max_length=254),
Description("We'll never share your email")
]
# Use in your application
@dataclass
class RegistrationForm:
username: Username
email: ValidEmail
age: Annotated[PositiveInt, Field(le=120), Label("Age")]
volume: PercentageSlider = 50
params = analyze_dataclass(RegistrationForm)
Best Practices
DO:
- Build reusable constraint libraries
- Override constraints for stricter validation
- Layer constraints from general to specific
WHEN OVERRIDING PATTERNS:
- You'll lose the original widget type
- Consider combining patterns manually instead
- Or use two-layer validation (UI + business logic)
DON'T:
- Expect multiple patterns to coexist (last wins)
- Create contradictory constraints (e.g.,
ge=100, le=50)