Universal Prefixed Literal IDs - type-safe, human-readable identifiers
Project description
UPLID
██╗ ██╗██████╗ ██╗ ██╗██████╗
██║ ██║██╔══██╗██║ ██║██╔══██╗
██║ ██║██████╔╝██║ ██║██║ ██║
██║ ██║██╔═══╝ ██║ ██║██║ ██║
╚██████╔╝██║ ███████╗██║██████╔╝
╚═════╝ ╚═╝ ╚══════╝╚═╝╚═════╝
Stripe-style IDs for Python.
# Before: WTF is this?
"550e8400-e29b-41d4-a716-446655440000"
# After: It's a user.
"usr_0M3xL9kQ7vR2nP5wY1jZ4c"
Install
pip install uplid
Requires Python 3.14+ and Pydantic 2.10+.
Quick Start
>>> from uplid import UPLID
>>> UPLID.generate("usr")
usr_0M3xL9kQ7vR2nP5wY1jZ4c
>>> UPLID.generate("ord")
ord_7x9KmNpQrStUvWxYz012Ab
Why UPLID?
Debuggable - See usr_ in your logs and instantly know it's a user, not an order, not a session, not a mystery.
# Your logs now:
"User usr_0M3xL9kQ7vR2nP5wY1jZ4c created order ord_1a2B3c4D5e6F7g..."
# vs the nightmare:
"User 550e8400-e29b-41d4... created order 7c9e6679-7425-40de..."
Type-safe - Your type checker catches user_id = order_id mistakes before they hit production.
UserId = UPLID[Literal["usr"]]
OrgId = UPLID[Literal["org"]]
def get_user(user_id: UserId) -> User: ...
get_user(org_id) # Type error! Caught by mypy/pyright/ty
Time-sortable - Built on UUIDv7. Sort by ID = sort by creation time. No extra column needed.
URL-safe - 26 characters, no special characters, no encoding. usr_0M3xL9kQ7vR2nP5wY1jZ4c
Minimal dependencies - Just Pydantic. UUID generation uses Python 3.14's stdlib uuid7().
Inspired by Stripe's prefixed IDs (
sk_live_...,cus_...,pi_...) - the same pattern trusted by millions of API calls daily.
Pydantic Integration
from typing import Literal
from pydantic import BaseModel, Field
from uplid import UPLID, factory
UserId = UPLID[Literal["usr"]]
class User(BaseModel):
id: UserId = Field(default_factory=factory(UserId))
name: str
user = User(name="Alice")
user.model_dump() # {"id": "usr_0M3xL9kQ7vR2nP5wY1jZ4c", "name": "Alice"}
User(id="org_xxx...", name="Bad") # ValidationError: wrong prefix
FastAPI Integration
from typing import Annotated, Literal
from fastapi import Depends, FastAPI, HTTPException
from uplid import UPLID, UPLIDError, parse
UserId = UPLID[Literal["usr"]]
parse_user_id = parse(UserId)
def validate_user_id(user_id: str) -> UserId:
try:
return parse_user_id(user_id)
except UPLIDError as e:
raise HTTPException(422, str(e)) from None
@app.get("/users/{user_id}")
def get_user(user_id: Annotated[UserId, Depends(validate_user_id)]) -> User:
... # user_id is validated and typed
SQLAlchemy Integration
from uplid import UPLID, factory
from uplid.sqlalchemy import uplid_column
UserId = UPLID[Literal["usr"]]
class User(Base):
__tablename__ = "users"
id: Mapped[UserId] = uplid_column(UserId, primary_key=True)
name: Mapped[str]
# Stores as TEXT, returns as UPLID objects
user = session.execute(select(User)).scalar_one()
user.id.prefix # "usr"
user.id.datetime # When the ID was created
SQLModel Integration
from uplid import UPLID, factory
from uplid.sqlalchemy import uplid_field
UserId = UPLID[Literal["usr"]]
class User(SQLModel, table=True):
id: UserId = uplid_field(UserId, default_factory=factory(UserId), primary_key=True)
name: str
user.model_dump() # {"id": "usr_...", "name": "Alice"} - Pydantic just works
Prefix Rules
Prefixes must be snake_case: lowercase letters and single underscores, cannot start/end with underscore, max 64 characters.
Examples: usr, api_key, org_member, sk_live
API Reference
UPLID[PREFIX]
uid = UPLID.generate("usr") # Generate new
uid = UPLID.from_string("usr_0M3xL9kQ7vR2nP5wY1jZ4c", "usr") # Parse
uid.prefix # "usr"
uid.uid # UUID object
uid.base62_uid # "0M3xL9kQ7vR2nP5wY1jZ4c"
uid.datetime # When created (from UUIDv7)
uid.timestamp # Unix timestamp
factory(UPLIDType) / parse(UPLIDType)
UserId = UPLID[Literal["usr"]]
UserIdFactory = factory(UserId) # For Pydantic default_factory
parse_user_id = parse(UserId) # For manual parsing, raises UPLIDError
UPLIDType
Protocol for functions accepting any UPLID:
def log_entity(id: UPLIDType) -> None:
print(f"{id.prefix} created at {id.datetime}")
uplid_column / uplid_field
from uplid.sqlalchemy import uplid_column, uplid_field
# SQLAlchemy
id: Mapped[UserId] = uplid_column(UserId, primary_key=True)
# SQLModel
id: UserId = uplid_field(UserId, default_factory=factory(UserId), primary_key=True)
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file uplid-1.1.0.tar.gz.
File metadata
- Download URL: uplid-1.1.0.tar.gz
- Upload date:
- Size: 9.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e27840f4faff8814355bf7163667c69ae0f3a86135762aac82d6b2aca9bda093
|
|
| MD5 |
1209f191e659f56a5215625e68c47300
|
|
| BLAKE2b-256 |
a06aa0569bd934c32269dbe525929c07a7183674caa518544903c448a3efd5c1
|
File details
Details for the file uplid-1.1.0-py3-none-any.whl.
File metadata
- Download URL: uplid-1.1.0-py3-none-any.whl
- Upload date:
- Size: 10.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e592e76b33c574055872e0a957ae59359308352947992c0603538545d81628a3
|
|
| MD5 |
9bee8fb3870c70f041499dd54fb76cdd
|
|
| BLAKE2b-256 |
748aa0086942ea2096c39a275bcb98f300002f852866ad2f501e17e343b346d2
|