Cheatsheet for Postgres-specific scalar types that aren’t JSONB/array.
Range types
from sqlalchemy.dialects.postgresql import (
INT4RANGE, INT8RANGE, NUMRANGE,
DATERANGE, TSRANGE, TSTZRANGE,
Range,
)
class Booking(Base):
__tablename__ = "bookings"
id: Mapped[int] = mapped_column(primary_key=True)
room_id: Mapped[int]
period: Mapped[Range] = mapped_column(TSTZRANGE)
Postgres ranges are [low, high) by default (inclusive low, exclusive high).
Construct ranges
from datetime import datetime
from sqlalchemy.dialects.postgresql import Range
period = Range(datetime(2026, 5, 1), datetime(2026, 5, 8), bounds="[)")
booking.period = period
Query operators
from sqlalchemy import text
# Overlaps (&&)
stmt = select(Booking).where(Booking.period.op("&&")(other_period))
# Contains element (@>)
stmt = select(Booking).where(Booking.period.contains(some_dt))
# Contained by (<@)
stmt = select(Booking).where(text("period <@ :p"), {"p": Range(...)})
# Lower / upper bound
stmt = select(Booking).where(func.lower(Booking.period) > t1)
stmt = select(Booking).where(func.upper(Booking.period) < t2)
Exclusion constraint (no overlap)
from sqlalchemy import ExcludeConstraint
class Booking(Base):
__tablename__ = "bookings"
id: Mapped[int] = mapped_column(primary_key=True)
room_id: Mapped[int]
period: Mapped[Range] = mapped_column(TSTZRANGE)
__table_args__ = (
ExcludeConstraint(("room_id", "="), ("period", "&&"), name="ex_no_overlap"),
)
DB-enforced: same room can’t have overlapping bookings.
INTERVAL
from datetime import timedelta
from sqlalchemy import Interval
class Subscription(Base):
duration: Mapped[timedelta] = mapped_column(Interval)
Postgres INTERVAL ↔ Python timedelta.
stmt = select(Subscription).where(Subscription.duration > timedelta(days=30))
INTERVAL arithmetic
from sqlalchemy import func, text
# Add interval to timestamp
stmt = select(Event).where(Event.starts_at + text("interval '1 hour'") > now)
stmt = select(Event).where(Event.ends_at - Event.starts_at > timedelta(hours=2))
citext (case-insensitive)
from sqlalchemy.dialects.postgresql import CITEXT
class User(Base):
email: Mapped[str] = mapped_column(CITEXT, unique=True)
'[email protected]' == '[email protected]' evaluates true.
Requires CREATE EXTENSION citext.
INET / CIDR
from sqlalchemy.dialects.postgresql import INET, CIDR
class Visitor(Base):
ip: Mapped[str] = mapped_column(INET)
subnet: Mapped[str | None] = mapped_column(CIDR)
INET queries
from sqlalchemy import text
# Subnet contains IP
stmt = select(Visitor).where(text("ip <<= :net"), {"net": "10.0.0.0/8"})
# Network family (4 or 6)
stmt = select(Visitor).where(text("family(ip) = 4"))
# Network address
stmt = select(func.host(Visitor.ip))
MAC addresses
from sqlalchemy.dialects.postgresql import MACADDR
class Device(Base):
mac: Mapped[str] = mapped_column(MACADDR)
money (avoid)
Postgres has money type but it’s locale-dependent. Prefer NUMERIC(12, 2) for storage; format in the app.
bytea (binary)
from sqlalchemy import LargeBinary
class Doc(Base):
content: Mapped[bytes] = mapped_column(LargeBinary)
For large binary: consider object storage (S3) + URL column.
UUID
import uuid
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy import text
class Doc(Base):
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
server_default=text("gen_random_uuid()"),
)
PG 13+: gen_random_uuid() built-in.
For UUIDv7 (sortable; great for primary keys with time ordering): use the uuid-osp extension or generate in Python.
ENUM
from sqlalchemy import Enum as SAEnum
import enum
class Role(str, enum.Enum):
USER = "user"; ADMIN = "admin"
class User(Base):
role: Mapped[Role] = mapped_column(SAEnum(Role, name="user_role", create_type=True))
Adding a value: ALTER TYPE user_role ADD VALUE 'editor' (PG 12+ supports in transaction).
For schema flexibility, VARCHAR + CHECK is easier.
tsvector (full-text)
from sqlalchemy.dialects.postgresql import TSVECTOR
class Post(Base):
title: Mapped[str]
body: Mapped[str]
search: Mapped[str] = mapped_column(
TSVECTOR,
Computed("to_tsvector('english', coalesce(title, '') || ' ' || coalesce(body, ''))", persisted=True),
)
Covered in detail in Cheatsheet 06 — FTS .
hstore (legacy; prefer JSONB)
from sqlalchemy.dialects.postgresql import HSTORE
class Doc(Base):
meta_: Mapped[dict] = mapped_column(HSTORE)
Older flat key-value type. JSONB superseded.
Common mistakes
- Bound syntax confusion
[)vs[]vs(). - Forgetting EXCLUDE requires a GIST index over the range column.
- Using citext on indexed PK column — sometimes interacts with cluster ordering.
- INTERVAL precision — sub-second sometimes lost across drivers.
Read this next
If you want my booking-with-no-overlap reference, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .