A pure Python library providing typed attributes with validation, change notifications, and configuration management for the IPython/Jupyter ecosystem.
Decorators and handlers for observing trait changes, validating values, and providing dynamic defaults. These enable reactive programming patterns and sophisticated validation logic in trait-based classes.
Decorators for registering functions to respond to trait changes.
def observe(*names, type='change'):
"""
Decorator to observe trait changes.
Registers a method to be called when specified traits change.
The decorated method receives a change dictionary with details
about the change event.
Parameters:
- *names: str - Trait names to observe (empty for all traits)
- type: str - Type of change to observe ('change' is most common)
Returns:
function - Decorator function
Usage:
@observe('trait_name', 'other_trait')
def _trait_changed(self, change):
# Handle change
pass
"""
def observe_compat(*names, type='change'):
"""
Backward-compatibility decorator for observers.
Provides compatibility with older observer patterns while
using the new observe decorator internally.
Parameters:
- *names: str - Trait names to observe
- type: str - Type of change to observe
Returns:
function - Decorator function
"""Decorators for registering cross-validation functions.
def validate(*names):
"""
Decorator to register cross-validator.
Registers a method to validate trait values before they are set.
The validator can modify the value or raise TraitError to reject it.
Parameters:
- *names: str - Trait names to validate
Returns:
function - Decorator function
Usage:
@validate('trait_name')
def _validate_trait(self, proposal):
# proposal is dict with 'value' key
value = proposal['value']
# Validate and possibly modify value
return validated_value
"""Decorators for providing dynamic default values.
def default(name):
"""
Decorator for dynamic default value generator.
Registers a method to provide default values for traits when
they are first accessed and no value has been set.
Parameters:
- name: str - Trait name to provide default for
Returns:
function - Decorator function
Usage:
@default('trait_name')
def _default_trait(self):
return computed_default_value
"""Classes representing different types of event handlers.
class EventHandler:
"""
Base class for event handlers.
Provides common functionality for all types of event handlers
including observer, validator, and default handlers.
"""
class ObserveHandler(EventHandler):
"""
Handler for observe decorator.
Manages observation of trait changes and dispatching
to registered handler methods.
"""
class ValidateHandler(EventHandler):
"""
Handler for validate decorator.
Manages validation of trait values before assignment
and dispatching to registered validator methods.
"""
class DefaultHandler(EventHandler):
"""
Handler for default decorator.
Manages generation of default values for traits
and dispatching to registered default methods.
"""from traitlets import HasTraits, Unicode, Int, observe
class Person(HasTraits):
name = Unicode()
age = Int()
@observe('name')
def _name_changed(self, change):
print(f"Name changed from '{change['old']}' to '{change['new']}'")
@observe('age')
def _age_changed(self, change):
print(f"Age changed from {change['old']} to {change['new']}")
@observe('name', 'age')
def _any_change(self, change):
print(f"Trait '{change['name']}' changed on {change['owner']}")
person = Person()
person.name = "Alice" # Triggers _name_changed and _any_change
person.age = 30 # Triggers _age_changed and _any_change
person.name = "Bob" # Triggers _name_changed and _any_changefrom traitlets import HasTraits, Unicode, observe
class Example(HasTraits):
value = Unicode()
@observe('value')
def _value_changed(self, change):
print("Change event details:")
print(f" name: {change['name']}") # 'value'
print(f" old: {change['old']}") # Previous value
print(f" new: {change['new']}") # New value
print(f" owner: {change['owner']}") # Self
print(f" type: {change['type']}") # 'change'
example = Example()
example.value = "initial" # old=u'', new='initial'
example.value = "updated" # old='initial', new='updated'from traitlets import HasTraits, Int, TraitError, validate
class Rectangle(HasTraits):
width = Int(min=0)
height = Int(min=0)
max_area = Int(default_value=1000)
@validate('width', 'height')
def _validate_dimensions(self, proposal):
value = proposal['value']
trait_name = proposal['trait'].name
# Get the other dimension
if trait_name == 'width':
other_dim = self.height
else:
other_dim = self.width
# Check if area would exceed maximum
if value * other_dim > self.max_area:
raise TraitError(f"Area would exceed maximum of {self.max_area}")
return value
rect = Rectangle(max_area=100)
rect.width = 10
rect.height = 8 # 10 * 8 = 80, within limit
# rect.height = 15 # Would raise TraitError (10 * 15 = 150 > 100)import time
from traitlets import HasTraits, Unicode, Float, default
class LogEntry(HasTraits):
message = Unicode()
timestamp = Float()
hostname = Unicode()
@default('timestamp')
def _default_timestamp(self):
return time.time()
@default('hostname')
def _default_hostname(self):
import socket
return socket.gethostname()
# Each instance gets current timestamp and hostname
entry1 = LogEntry(message="First log")
time.sleep(0.1)
entry2 = LogEntry(message="Second log")
print(entry1.timestamp != entry2.timestamp) # True - different times
print(entry1.hostname == entry2.hostname) # True - same hostfrom traitlets import HasTraits, Unicode, Bool, default
class User(HasTraits):
username = Unicode()
email = Unicode()
is_admin = Bool(default_value=False)
display_name = Unicode()
@default('display_name')
def _default_display_name(self):
if self.email:
return self.email.split('@')[0]
elif self.username:
return self.username.title()
else:
return "Anonymous"
user1 = User(email="alice@example.com")
print(user1.display_name) # "alice"
user2 = User(username="bob_smith")
print(user2.display_name) # "Bob_Smith"
user3 = User()
print(user3.display_name) # "Anonymous"from traitlets import HasTraits, Unicode, validate
class NormalizedText(HasTraits):
text = Unicode()
@validate('text')
def _validate_text(self, proposal):
value = proposal['value']
# Normalize whitespace and case
normalized = ' '.join(value.split()).lower()
# Remove forbidden characters
forbidden = ['<', '>', '&']
for char in forbidden:
normalized = normalized.replace(char, '')
return normalized
text_obj = NormalizedText()
text_obj.text = " Hello WORLD <script> "
print(text_obj.text) # "hello world script"from traitlets import HasTraits, Unicode, Int, observe, All
class Monitored(HasTraits):
name = Unicode()
value = Int()
description = Unicode()
@observe(All) # Observe all traits
def _any_trait_changed(self, change):
print(f"Any trait changed: {change['name']} = {change['new']}")
@observe(All, type='change') # Explicit change type
def _log_changes(self, change):
import datetime
timestamp = datetime.datetime.now().isoformat()
print(f"[{timestamp}] {change['name']}: {change['old']} -> {change['new']}")
obj = Monitored()
obj.name = "test" # Triggers both observers
obj.value = 42 # Triggers both observers
obj.description = "demo" # Triggers both observersfrom traitlets import HasTraits, Unicode, Int, List, TraitError, validate, observe
class Project(HasTraits):
name = Unicode()
priority = Int(min=1, max=5)
tags = List(Unicode())
assignees = List(Unicode())
@validate('name')
def _validate_name(self, proposal):
name = proposal['value']
# Must be non-empty and alphanumeric with underscores
if not name or not name.replace('_', '').isalnum():
raise TraitError("Name must be alphanumeric with underscores only")
return name.lower() # Normalize to lowercase
@validate('tags')
def _validate_tags(self, proposal):
tags = proposal['value']
# Remove duplicates and normalize
normalized_tags = list(set(tag.lower().strip() for tag in tags))
# Limit to 5 tags maximum
if len(normalized_tags) > 5:
raise TraitError("Maximum 5 tags allowed")
return normalized_tags
@observe('priority')
def _priority_changed(self, change):
if change['new'] >= 4:
print(f"High priority project: {self.name}")
project = Project()
project.name = "My_Project_123" # Becomes "my_project_123"
project.priority = 4 # Triggers high priority message
project.tags = ["Python", "WEB", "python", "api", "REST"] # Normalized and deduplicated
print(project.tags) # ['python', 'web', 'api', 'rest']Install with Tessl CLI
npx tessl i tessl/pypi-traitlets