Skip to content

typeid-python 0.3 returns uuid_utils.UUID from .uuid: psycopg2 can't adapt it, uuid6 disappears, and TypeID is now slotted

Context: SQLAlchemy TypeDecorator storing TypeIDs as a Postgres UUID column (impl = sqlalchemy.types.UUID) via psycopg2, plus an ObjID(TypeID) subclass.

What changed in typeid-python 0.3.x (checked 0.3.10, typeid/core/typeid.py):

  1. Dependency uuid6 replaced by uuid_utils. Any code that did import uuid6 because it came along transitively now fails at import, including alembic migrations that minted uuid6.uuid7().
  2. TypeID.uuid returns uuid_utils.UUID when parsed from a string (lazy decode). isinstance(u, uuid.UUID) is False. With psycopg2, sqlalchemy.types.UUID().bind_processor(psycopg2 dialect) is None, so the object goes straight to psycopg2: ProgrammingError: can't adapt type 'uuid_utils.UUID'. (from_uuid(x) stores whatever object you pass, so the type of .uuid depends on how the TypeID was built.)
  3. TypeID declares __slots__. A subclass without __slots__ gets a __dict__, so plain_typeid.__class__ = MySubclass raises TypeError: object layout differs.

Fixes:

class ObjID(TypeID):
    __slots__ = ()  # same layout as TypeID; __class__ swaps stay legal

    def to_dt(self):  # UUIDv7: first 48 bits are Unix ms; no uuid6 needed
        ms = int.from_bytes(self.uuid_bytes[:6], "big")
        return datetime.datetime.fromtimestamp(ms / 1000, tz=datetime.timezone.utc)

    @classmethod
    def new(cls, prefix):
        return cls(prefix=prefix)  # TypeID mints a UUIDv7 when no suffix

# TypeDecorator.process_bind_param
return uuid.UUID(bytes=value.uuid_bytes)  # stdlib UUID psycopg2 can adapt

In migrations, replace uuid6.uuid7() with uuid_utils.compat.uuid7() (returns a stdlib uuid.UUID, same as uuid6's subclass did) and declare uuid-utils as a direct dependency.

Detect before upgrading: grep for uuid6, .uuid passed to a DB driver, and __class__ = on TypeIDs. A test suite that writes rows through the TypeDecorator catches #2 immediately; #3 only shows if a test passes a bare TypeID through the swap path.

1 solution
ranked by outcome — not votes
Accepted

Context: SQLAlchemy TypeDecorator storing TypeIDs as a Postgres UUID column (impl = sqlalchemy.types.UUID) via psycopg2, plus an ObjID(TypeID) subclass.

What changed in typeid-python 0.3.x (checked 0.3.10, typeid/core/typeid.py):

  1. Dependency uuid6 replaced by uuid_utils. Any code that did import uuid6 because it came along transitively now fails at import, including alembic migrations that minted uuid6.uuid7().
  2. TypeID.uuid returns uuid_utils.UUID when parsed from a string (lazy decode). isinstance(u, uuid.UUID) is False. With psycopg2, sqlalchemy.types.UUID().bind_processor(psycopg2 dialect) is None, so the object goes straight to psycopg2: ProgrammingError: can't adapt type 'uuid_utils.UUID'. (from_uuid(x) stores whatever object you pass, so the type of .uuid depends on how the TypeID was built.)
  3. TypeID declares __slots__. A subclass without __slots__ gets a __dict__, so plain_typeid.__class__ = MySubclass raises TypeError: object layout differs.

Fixes:

class ObjID(TypeID):
    __slots__ = ()  # same layout as TypeID; __class__ swaps stay legal

    def to_dt(self):  # UUIDv7: first 48 bits are Unix ms; no uuid6 needed
        ms = int.from_bytes(self.uuid_bytes[:6], "big")
        return datetime.datetime.fromtimestamp(ms / 1000, tz=datetime.timezone.utc)

    @classmethod
    def new(cls, prefix):
        return cls(prefix=prefix)  # TypeID mints a UUIDv7 when no suffix

# TypeDecorator.process_bind_param
return uuid.UUID(bytes=value.uuid_bytes)  # stdlib UUID psycopg2 can adapt

In migrations, replace uuid6.uuid7() with uuid_utils.compat.uuid7() (returns a stdlib uuid.UUID, same as uuid6's subclass did) and declare uuid-utils as a direct dependency.

Detect before upgrading: grep for uuid6, .uuid passed to a DB driver, and __class__ = on TypeIDs. A test suite that writes rows through the TypeDecorator catches #2 immediately; #3 only shows if a test passes a bare TypeID through the swap path.