Types: Type mapping system for converting between Python, JSON, and SQLite types.
This module provides a registry of known types and their conversion functions
for seamless data transformation between different representations.
Core Concepts
KnownType: A registered type with bidirectional mappings (string, JSON, DB)
KnownTypesMap: Registry of all known types, accessed via KNOWN_TYPES
JsonBase: Pydantic BaseModel with type discrimination via type_gref
Type Mappings
Each KnownType provides three MapPairs for converting to/from:
- string: For CLI arguments, environment variables, config files
- json: For JSON serialization (API responses, storage)
- db: For SQLite storage (maps to INTEGER, REAL, TEXT, or BLOB)
Built-in Types
Primitives: int, float, str, bool, bytes
Dates: date, datetime (ISO format strings)
Enums: Enum (string values), IntEnum (integer values)
Pydantic: BaseModel, JsonBase (JSON dicts)
Paths: Path (string representation)
Usage
from lythonic.types import KNOWN_TYPES, KnownType
# Resolve a type
kt = KNOWN_TYPES.resolve_type(datetime)
# Convert from string
dt = kt.string.map_from("2024-01-15T10:30:00")
# Convert to JSON
json_val = kt.json.map_to(dt) # "2024-01-15T10:30:00"
# Convert to DB
db_val = kt.db.map_to(dt) # "2024-01-15T10:30:00"
Simple Types
Types whose string representation is a plain, URL-safe scalar are marked
simple_type=True on KnownTypeArgs / KnownType. This is auto-detected
during KNOWN_TYPES.register() for primitives (int, float, bool, str),
date, datetime, and Path. Used by lythonic.compose.cached to validate
that method parameters can serve as cache key columns.
kt = KNOWN_TYPES.resolve_type(int)
assert kt.simple_type # True — plain scalar
kt = KNOWN_TYPES.resolve_type(bytes)
assert not kt.simple_type # False — base64-encoded, not a plain scalar
Registering Custom Types
from lythonic.types import KNOWN_TYPES, KnownTypeArgs
KNOWN_TYPES.register(
KnownTypeArgs(
concrete_type=MyType,
map_from_string=MyType.parse,
map_to_string=str,
simple_type=True, # if the string form is URL-safe
)
)
JsonBase
JsonBase extends Pydantic's BaseModel with automatic type discrimination:
from lythonic.types import JsonBase
class Dog(JsonBase):
name: str
class Cat(JsonBase):
name: str
# Serialize includes type reference
dog = Dog(name="Rex")
data = dog.to_json() # {"type_gref": "mymodule:Dog", "name": "Rex"}
# Deserialize resolves correct type
animal = JsonBase.from_json(data) # Returns Dog instance
KNOWN_TYPES = KnownTypesMap()
module-attribute
KnownType
Source code in src/lythonic/types.py
| class KnownType:
name: str
aliases: set[str]
type_: type
is_abstract: bool
string: MapPair[str]
json: MapPair[Any]
db: MapPair[Any]
simple_type: bool
@classmethod
def ensure(cls, type_: type | str | KnownType) -> KnownType:
if isinstance(type_, KnownType):
return type_
return KNOWN_TYPES.resolve_type(type_)
def __init__(
self,
args: KnownTypeArgs,
) -> None:
assert not args.is_factory, "factory types cannot be initialized directly"
self.type_ = args.get_type()
self.is_abstract = args.is_abstract()
args.resolve_the_rest()
assert (
args.db_type is not None
and args.map_from_db is not None
and args.map_to_db is not None
and args.json_type is not None
and args.map_from_json is not None
and args.map_to_json is not None
and args.map_from_string is not None
and args.map_to_string is not None
), f"{self.type_} has not been resolved"
self.string = MapPair(args.map_from_string, args.map_to_string, str) # pyright: ignore
self.json = MapPair(args.map_from_json, args.map_to_json, args.json_type)
self.db = MapPair(args.map_from_db, args.map_to_db, args.db_type)
self.name = (args.name if args.name is not None else self.type_.__name__).lower()
self.aliases = set()
self.aliases.add(self.name)
if args.aliases is not None:
for alias in args.aliases:
self.aliases.add(alias.lower())
self.simple_type = args.simple_type
def get_type(self) -> type:
return self.type_
@property
def db_type_info(self) -> DbTypeInfo:
return DbTypeInfo.from_type(self.db.target_type)
@override
def __repr__(self) -> str:
return f"KnownType({self.name!r}, {self.type_!r})"
|
KnownTypeArgs
Bases: BaseModel
Source code in src/lythonic/types.py
| class KnownTypeArgs(BaseModel):
map_from_string: Callable[[str], Any] | None = None
map_to_string: Callable[[Any], str] | None = None
map_from_json: Callable[[Any], Any] | None = None
map_to_json: Callable[[Any], Any] | None = None
map_from_db: Callable[[Any], Any] | None = None
map_to_db: Callable[[Any], Any] | None = None
db_type: type | None = None
json_type: type | None = None
concrete_type: type | None = None
abstract_type: type | None = None
is_factory: bool = False
simple_type: bool = False
name: str | None = None
aliases: list[str] | None = None
def get_type(self) -> type:
assert self.abstract_type is not None or self.concrete_type is not None
if self.concrete_type is not None:
assert self.abstract_type is None and not self.is_factory, (
"concrete types cannot be factories"
)
return self.concrete_type
assert self.abstract_type is not None, " Redundant but no other way to shut up pyright"
return self.abstract_type
def is_abstract(self) -> bool:
return self.abstract_type is self.get_type()
def build_concrete_type(self, concrete_type: type) -> Self:
assert self.is_factory, "only factory types can build concrete types"
super_type = self.get_type()
assert issubclass(concrete_type, super_type), (
f"concrete type {concrete_type} is not a subclass of abstract type {super_type}"
)
clone = self.model_copy()
clone.concrete_type = concrete_type
clone.abstract_type = None
clone.is_factory = False
def remap_constructor_from_super_type_to_concrete_type(
x: Callable[[Any], Any] | None,
) -> Callable[[Any], Any] | None:
if x is None:
return None
if x is super_type: # switch to constructor of concrete type
return concrete_type
if (
inspect.ismethod(x) and x.__self__ is super_type
): # switch to class method of concrete type
return getattr(concrete_type, x.__name__)
return x # leave as is
clone.map_from_string = remap_constructor_from_super_type_to_concrete_type(
clone.map_from_string
)
clone.map_from_json = remap_constructor_from_super_type_to_concrete_type(
clone.map_from_json
)
clone.map_from_db = remap_constructor_from_super_type_to_concrete_type(clone.map_from_db)
return clone
def _set_string_defaults(self) -> None:
if self.db_type is None:
self.db_type = str
if self.json_type is None:
self.json_type = str
self.map_from_string = passthru_none(self.get_type(), if_none=self.map_from_string)
self.map_to_string = passthru_none(str, if_none=self.map_to_string)
self.map_from_json = passthru_none(self.map_from_string, if_none=self.map_from_json)
self.map_to_json = passthru_none(self.map_to_string, if_none=self.map_to_json)
self.map_from_db = passthru_none(self.map_from_string, if_none=self.map_from_db)
self.map_to_db = passthru_none(self.map_to_string, if_none=self.map_to_db)
def _set_db_defaults(self) -> None:
assert self.db_type is not None and self.map_from_db is not None
self.map_to_db = passthru_none(self.db_type, if_none=self.map_to_db)
if self.db_type is bytes:
if self.json_type is None:
self.json_type = str
self.map_from_json = passthru_none(ensure_bytes, if_none=self.map_from_json)
self.map_to_json = passthru_none(encode_base64, if_none=self.map_to_json)
self.map_from_string = passthru_none(
ensure_bytes, self.map_from_db, if_none=self.map_from_string
)
self.map_to_string = passthru_none(
self.map_to_db, encode_base64, if_none=self.map_to_string
)
else:
if self.json_type is None:
self.json_type = self.db_type
self.map_from_json = passthru_none(self.map_from_db, if_none=self.map_from_json)
self.map_to_json = passthru_none(self.map_to_db, if_none=self.map_to_json)
self.map_from_string = passthru_none(
self.db_type, self.map_from_db, if_none=self.map_from_string
)
self.map_to_string = passthru_none(self.map_to_db, str, if_none=self.map_to_string)
def _set_json_defaults(self) -> bool:
if self.map_from_json is not None and self.map_to_json is not None:
if self.db_type is None:
self.db_type = str
if self.json_type is None:
self.json_type = dict
self.map_from_json = passthru_none(self.map_from_json)
self.map_to_json = passthru_none(self.map_to_json)
self.map_from_string = passthru_none(
json_loads, self.map_from_json, if_none=self.map_from_string
)
self.map_to_string = passthru_none(
self.map_to_json, json_dumps, if_none=self.map_to_string
)
self.map_from_db = passthru_none(self.map_from_string, if_none=self.map_from_db)
self.map_to_db = passthru_none(self.map_to_string, if_none=self.map_to_db)
return True
return False
def resolve_the_rest(self) -> Self:
assert not self.is_factory, "factory types cannot resolve the rest"
if self.concrete_type is not None:
assert self.abstract_type is None
if self.map_from_string is not None or self.map_to_string is not None:
self._set_string_defaults()
else:
if self.db_type is None and DbTypeInfo.is_db_type(self.concrete_type): # pyright: ignore
self.db_type = self.concrete_type
if self.db_type is not None:
self.map_from_db = passthru_none(
self.db_type, self.concrete_type, if_none=self.map_from_db
)
self._set_db_defaults()
else:
self._set_json_defaults()
else:
assert self.abstract_type is not None
assert (
self.map_from_string is not None
or self.map_from_json is not None
or self.map_from_db is not None
)
if self.map_from_string is not None:
self._set_string_defaults()
elif self._set_json_defaults():
pass
elif self.db_type is not None:
assert self.map_from_db is not None
self.map_from_db = passthru_none(self.map_from_db)
self._set_db_defaults()
return self
|
KnownTypesMap
Source code in src/lythonic/types.py
| class KnownTypesMap:
types: dict[str, KnownType]
types_by_type: dict[type, KnownType]
abstract_types: AbstractTypeHeap
def __init__(self) -> None:
self.types = {}
self.types_by_type = {}
self.abstract_types = AbstractTypeHeap.root()
def register(self, *array_of_args: KnownTypeArgs) -> None:
for args in array_of_args:
if not args.simple_type:
t = args.concrete_type or args.abstract_type
if t is not None and is_primitive(t):
args.simple_type = True
if not args.is_factory:
self.register_type(KnownType(args))
else:
self.abstract_types.add(args)
def register_type(self, t: KnownType) -> None:
for alias in t.aliases:
if alias not in self.types:
self.types[alias] = t
assert t.get_type() not in self.types_by_type, f"Duplicate type {t.get_type()}"
self.types_by_type[t.type_] = t
if t.is_abstract:
self.abstract_types.add(t)
def resolve_type(self, type_: type | str) -> KnownType:
"""
>>> KNOWN_TYPES.resolve_type(int)
KnownType('int', <class 'int'>)
>>> KNOWN_TYPES.resolve_type("int")
KnownType('int', <class 'int'>)
>>> KNOWN_TYPES.resolve_type("zzz")
Traceback (most recent call last):
...
ValueError: Unknown type zzz
>>> KNOWN_TYPES.resolve_type(dict)
Traceback (most recent call last):
...
ValueError: Unknown type <class 'dict'>
>>> KNOWN_TYPES.resolve_type(3)
Traceback (most recent call last):
...
TypeError: issubclass() arg 1 must be a class
"""
if isinstance(type_, str):
type_ = type_.lower()
if type_ in self.types:
return self.types[type_]
else:
if type_ in self.types_by_type:
return self.types_by_type[type_]
found, super_type = self.abstract_types.find(type_)
assert found is None, (
f"Why it is not in types_by_type found={found}, super_type={super_type}"
)
if super_type is not None and not super_type.is_root():
assert super_type.ktype is not None
if isinstance(super_type.ktype, KnownTypeArgs):
args: KnownTypeArgs = super_type.ktype
ktype: KnownType = KnownType(args.build_concrete_type(type_))
self.register_type(ktype)
return ktype
assert isinstance(super_type.ktype, KnownType), (
f"super_type.ktype is not a KnownType: {super_type.ktype}"
)
return super_type.ktype
raise ValueError(f"Unknown type {type_}")
|
resolve_type(type_)
>>> KNOWN_TYPES.resolve_type(int)
KnownType('int', <class 'int'>)
>>> KNOWN_TYPES.resolve_type("int")
KnownType('int', <class 'int'>)
>>> KNOWN_TYPES.resolve_type("zzz")
Traceback (most recent call last):
...
ValueError: Unknown type zzz
>>> KNOWN_TYPES.resolve_type(dict)
Traceback (most recent call last):
...
ValueError: Unknown type <class 'dict'>
>>> KNOWN_TYPES.resolve_type(3)
Traceback (most recent call last):
...
TypeError: issubclass() arg 1 must be a class
Source code in src/lythonic/types.py
| def resolve_type(self, type_: type | str) -> KnownType:
"""
>>> KNOWN_TYPES.resolve_type(int)
KnownType('int', <class 'int'>)
>>> KNOWN_TYPES.resolve_type("int")
KnownType('int', <class 'int'>)
>>> KNOWN_TYPES.resolve_type("zzz")
Traceback (most recent call last):
...
ValueError: Unknown type zzz
>>> KNOWN_TYPES.resolve_type(dict)
Traceback (most recent call last):
...
ValueError: Unknown type <class 'dict'>
>>> KNOWN_TYPES.resolve_type(3)
Traceback (most recent call last):
...
TypeError: issubclass() arg 1 must be a class
"""
if isinstance(type_, str):
type_ = type_.lower()
if type_ in self.types:
return self.types[type_]
else:
if type_ in self.types_by_type:
return self.types_by_type[type_]
found, super_type = self.abstract_types.find(type_)
assert found is None, (
f"Why it is not in types_by_type found={found}, super_type={super_type}"
)
if super_type is not None and not super_type.is_root():
assert super_type.ktype is not None
if isinstance(super_type.ktype, KnownTypeArgs):
args: KnownTypeArgs = super_type.ktype
ktype: KnownType = KnownType(args.build_concrete_type(type_))
self.register_type(ktype)
return ktype
assert isinstance(super_type.ktype, KnownType), (
f"super_type.ktype is not a KnownType: {super_type.ktype}"
)
return super_type.ktype
raise ValueError(f"Unknown type {type_}")
|
JsonBase
Bases: BaseModel
Source code in src/lythonic/types.py
| class JsonBase(BaseModel):
type_gref: GRef | None = None
@model_validator(mode="before")
@classmethod
def set_gref(cls, data: dict[str, Any]) -> dict[str, Any]:
data["type_gref"] = str(GlobalRef(cls))
return data
@classmethod
def from_json(cls, json: dict[str, Any]) -> Self:
if "type_gref" in json:
ref = GlobalRef(json["type_gref"])
cls_from_ref = ref.get_instance()
if issubclass(cls_from_ref, cls):
return cls_from_ref.model_validate(json)
return cls.model_validate(json)
def to_json(self) -> dict[str, Any]:
return base_model_to_json(self)
|
DbTypeInfo
Bases: Enum
Source code in src/lythonic/types.py
| class DbTypeInfo(Enum):
def __init__(self, input_types: tuple[type, ...], target_type: type):
assert isinstance(input_types, tuple), f"input_types must be tuple, not {type(input_types)}"
cls = self.__class__
if not hasattr(cls, "_value2member_map_"):
cls._value2member_map_ = {}
member_map = cls._value2member_map_
for k in input_types:
assert k not in member_map, f"Duplicate input type {k} in {self.name}"
member_map[k] = self
self.input_types = input_types
self.target_type = target_type
INTEGER = ((int, bool), int)
REAL = ((float,), float)
TEXT = ((str,), str)
BLOB = ((bytes,), bytes)
@classmethod
def from_type(cls, t: type) -> DbTypeInfo:
return cast(
DbTypeInfo,
cls._value2member_map_[t],
)
@classmethod
def is_db_type(cls, type_: type) -> bool:
"""could be used in db as it is"""
return type_ in cls._value2member_map_
|
is_db_type(type_)
classmethod
could be used in db as it is
Source code in src/lythonic/types.py
| @classmethod
def is_db_type(cls, type_: type) -> bool:
"""could be used in db as it is"""
return type_ in cls._value2member_map_
|
MapPair
Bases: Generic[T]
Source code in src/lythonic/types.py
| class MapPair(Generic[T]):
target_type: type
map_from: Callable[[T], Any]
map_to: Callable[[Any], T]
def __init__(
self, map_from: Callable[[T], Any], map_to: Callable[[Any], T], target_type: type
) -> None:
self.map_from = map_from
self.map_to = map_to
self.target_type = target_type
|
is_primitive(type_)
could be used in json and db as it is
Source code in src/lythonic/types.py
| def is_primitive(type_: type) -> bool:
"""could be used in json and db as it is"""
return type_ in (int, float, bool, str)
|