Classno is a lightweight and extensible Python library for data modeling, schema definition, and validation. It provides a clean and intuitive way to define data classes with various features like type validation, immutability, private fields, and automatic type casting.
- Type hints validation - Runtime type checking for all fields
- Immutable objects - Create frozen, hashable instances
- Private fields - Write-once, read-only field access patterns
- Automatic type casting - Lossy autocast between compatible types
- Customizable comparison - Control equality, hashing, and ordering behavior
- Default values and factories - Safe handling of mutable defaults
- Nested object support - Deep validation and casting of complex structures
- Slots optimization - Reduced memory footprint
- Rich comparison methods - Full ordering support
- Union and Optional types - First-class support for
Optional[T]andUnion[T1, T2] - Extended collection types - Support for
frozenset,deque,defaultdict, and more
pip install classnofrom classno import Classno, field
class User(Classno):
name: str
age: int = 0
email: str = field(default="")
# Create an instance
user = User(name="John", age=30)Features can be enabled by setting the __features__ class attribute:
from classno import Classno, Features
class Config(Classno):
__features__ = Features.VALIDATION | Features.FROZEN
host: str
port: int = 8080Available features:
Features.EQ- Enable equality comparisonFeatures.ORDER- Enable ordering operationsFeatures.HASH- Make instances hashableFeatures.SLOTS- Use slots for memory optimizationFeatures.FROZEN- Make instances immutableFeatures.PRIVATE- Enable private field accessFeatures.VALIDATION- Enable type validationFeatures.LOSSY_AUTOCAST- Enable automatic type casting
Fields can be configured using the field() function:
from classno import Classno, field
from datetime import datetime
class Post(Classno):
title: str
content: str = ""
created_at: datetime = field(default_factory=datetime.now)
metadata: dict = field(default_factory=dict, metadata={"indexed": True})Important: Always use default_factory for mutable defaults (list, dict, set) to avoid shared state issues:
# ❌ WRONG - This will raise an error
class Wrong(Classno):
tags: list = []
# ✅ CORRECT - Use default_factory
class Correct(Classno):
tags: list = field(default_factory=list)class ValidatedModel(Classno):
__features__ = Features.VALIDATION
numbers: list[int]
mapping: dict[str, float]
# This will raise TypeError if types don't match
model = ValidatedModel(
numbers=[1, 2, 3],
mapping={"a": 1.0, "b": 2.0}
)Full support for Optional and Union types with proper validation:
from typing import Optional, Union
class FlexibleModel(Classno):
__features__ = Features.VALIDATION
# Optional field - can be None
optional_name: Optional[str] = None
# Union type - can be str or int
identifier: Union[str, int]
# Complex unions
data: Union[list[str], dict[str, int], None] = None
model = FlexibleModel(optional_name=None, identifier=42)Support for Python's extended collection types:
from collections import deque, defaultdict, Counter
from typing import Optional
class AdvancedCollections(Classno):
__features__ = Features.VALIDATION | Features.LOSSY_AUTOCAST
# Immutable set
unique_ids: frozenset[int] = field(default_factory=frozenset)
# Double-ended queue
history: deque[str] = field(default_factory=deque)
# Dict with default values
counters: defaultdict[str, int] = field(
default_factory=lambda: defaultdict(int)
)
# Counter for frequency tracking
word_counts: Counter[str] = field(default_factory=Counter)class ImmutableConfig(Classno):
__features__ = Features.IMMUTABLE # Combines FROZEN, SLOTS, and HASH
host: str
port: int = 8080
config = ImmutableConfig(host="localhost")
# Attempting to modify will raise an exception
config.port = 9000 # Raises Exceptionclass PrivateFields(Classno):
__features__ = Features.PRIVATE
name: str
secret: str # Can only be accessed with _secret prefix for rw, secret for ro
obj = PrivateFields(name="public")
obj._secret = "hidden" # OK
obj.secret # OK
obj.secret = "hidden" # Raises Exceptionclass Address(Classno):
street: str
city: str
class Person(Classno):
name: str
address: Address
# Create nested structure
person = Person(
name="John",
address=Address(street="123 Main St", city="Boston")
)Enable automatic type conversion with LOSSY_AUTOCAST:
class AutoCastModel(Classno):
__features__ = Features.LOSSY_AUTOCAST
count: int
price: float
active: bool
# Automatic casting from compatible types
model = AutoCastModel(
count="42", # str → int
price="19.99", # str → float
active="true" # str → bool
)
# model.count == 42 (int)
# model.price == 19.99 (float)
# model.active == True (bool)Features can be combined for powerful data modeling:
class RobustModel(Classno):
# First cast, then validate
__features__ = Features.LOSSY_AUTOCAST | Features.VALIDATION | Features.FROZEN
user_id: int
tags: list[str] = field(default_factory=list)
# This works: "42" is cast to 42, validated, then frozen
model = RobustModel(user_id="42", tags=["python", "classno"])Feature execution order:
LOSSY_AUTOCAST- Values are automatically cast to target typesVALIDATION- Type validation is performed on cast valuesFROZEN- Object becomes immutable after validation
Control which fields participate in equality, hashing, and ordering:
class CustomCompare(Classno):
__features__ = Features.EQ | Features.HASH | Features.ORDER
# Use tuples for ordered keys (order matters for comparison)
__hash_keys__ = ("id",) # Only id used for hashing
__eq_keys__ = ("id", "name") # id and name for equality
__order_keys__ = ("priority", "name") # Sort by priority, then name
id: int
name: str
priority: int = 0
description: str = ""
# Two objects are equal if id and name match
obj1 = CustomCompare(id=1, name="A", description="First")
obj2 = CustomCompare(id=1, name="A", description="Second")
assert obj1 == obj2 # True - description is ignored
# Ordering uses priority first, then name
items = [
CustomCompare(id=1, name="B", priority=2),
CustomCompare(id=2, name="A", priority=1),
]
sorted_items = sorted(items) # Sorted by priority, then name- Use type hints for all fields - Enables validation and better IDE support
- Use
default_factoryfor mutable defaults - Always usefield(default_factory=list)instead offield(default=[]) - Enable appropriate features - Only enable features you need (e.g.,
VALIDATIONin development,FROZENfor config) - Use
Features.SLOTS- Reduces memory usage by ~40% for classes with many instances - Combine
LOSSY_AUTOCASTwithVALIDATION- Cast first, then validate for robust data handling - Use tuples for comparison keys -
__eq_keys__ = ("id", "name")not["id", "name"] - Leverage
OptionalandUnion- Use proper type hints for flexible APIs
Classno provides clear, actionable error messages:
# Validation error
class Model(Classno):
__features__ = Features.VALIDATION
age: int
try:
Model(age="not a number")
except ValidationError as e:
# "Validation error for field 'age': expected int, got 'not a number' of type str"
pass
# Mutable default error
try:
class Wrong(Classno):
tags: list = [] # ❌ Error at class definition time
except ValueError as e:
# "Mutable default values are not allowed. Found list [].
# Use default_factory=lambda: [] instead to avoid shared state issues."
pass
# Frozen modification error
class Frozen(Classno):
__features__ = Features.FROZEN
value: int
obj = Frozen(value=42)
try:
obj.value = 100
except AttributeError as e:
# "Cannot modify attributes of frozen class Frozen"
passThe library raises appropriate exceptions for:
- ValidationError - Type validation failures
- TypeError - Type mismatch during casting or validation
- ValueError - Invalid field configurations (e.g., mutable defaults)
- AttributeError - Immutability violations (FROZEN), invalid field access (PRIVATE)
Features and fields are properly inherited:
class BaseModel(Classno):
__features__ = Features.VALIDATION
id: int
class UserModel(BaseModel):
# Inherits VALIDATION feature and id field
name: str
email: str
user = UserModel(id=1, name="John", email="john@example.com")Convert objects to dictionaries for JSON serialization:
class User(Classno):
name: str
age: int
user = User(name="John", age=30)
data = user.as_dict() # {"name": "John", "age": 30}Objects support both shallow and deep copying:
import copy
original = User(name="John", age=30)
shallow = copy.copy(original)
deep = copy.deepcopy(original)Run the comprehensive test suite:
# Install development dependencies
poetry install
# Run all tests
poetry run poe tests
# Run fast tests (skip slow tests)
poetry run poe test-fast
# Run with coverage
poetry run poe test-all
# Run specific test categories
poetry run poe test-unit # Unit tests
poetry run poe test-integration # Integration tests
poetry run poe test-edge-cases # Edge case tests- Dmitriy Kudryavtsev - author - kuderr