GUID Type

Source module: fastapi_utils.guid_type


The two most common types used for primary keys in database tables are integers and UUIDs (sometimes referred to as GUIDs).

There are a number of tradeoffs to make when deciding whether to use integers vs. UUIDs, including:

  • UUIDs don’t reveal anything about the number of records in a table
  • UUIDs are practically impossible for an adversary to guess (though you shouldn’t rely solely on that for security!)
  • UUIDs are harder to communicate/remember
  • UUIDs may result in worse performance for certain access patterns due to the random ordering

You’ll have to decide based on your application which is right for you, but if you want to use UUIDs/GUIDs for your primary keys, there are some difficulties to navigate.

Challenges using UUID-valued primary keys with sqlalchemy

Python has support for UUIDs in the standard library, and most relational databases have good support for them as well.

However, if you want a database-agnostic or database-driver-agnostic type, you may run into challenges.

In particular, the postgres-compatible UUID type provided by sqlalchemy (sqlalchemy.dialects.postgresql.UUID) will not work with other databases, and it also doesn’t come with a way to set a server-default, meaning that you’ll always need to take responsibility for generating an ID in your application code.

Even worse, if you try to use the postgres-compatible UUID type simultaneously with both sqlalchemy and the encode/databases package, you may run into issues where queries using one require you to set UUID(as_uuid=True), when declaring the column, and the other requires you to declare the table using UUID(as_uuid=False).

Fortunately, sqlalchemy provides a backend-agnostic implementation of GUID type that uses the postgres-specific UUID type when possible, and more carefully parses the result to ensure uuid.UUID isn’t called on something that is already a uuid.UUID (which raises an error).

For convenience, this package includes this GUID type, along with conveniences for setting up server defaults for primary keys of this type.

Using GUID

You can create a sqlalchemy table with a GUID as a primary key using the declarative API like this:

import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base

from fastapi_utils.guid_type import GUID

Base = declarative_base()


class User(Base):
    __tablename__ = "user"
    id = sa.Column(GUID, primary_key=True)
    name = sa.Column(sa.String, nullable=False)
    related_id = sa.Column(GUID)  # a nullable, related field

Server Default

If you want to add a server default, it will no longer be backend-agnostic, but you can use fastapi_utils.guid_type.GUID_SERVER_DEFAULT_POSTGRESQL:

import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base

from fastapi_utils.guid_type import GUID, GUID_SERVER_DEFAULT_POSTGRESQL

Base = declarative_base()


class User(Base):
    __tablename__ = "user"
    id = sa.Column(
        GUID,
        primary_key=True,
        server_default=GUID_SERVER_DEFAULT_POSTGRESQL
    )
    name = sa.Column(sa.String, nullable=False)
    related_id = sa.Column(GUID)

(Behind the scenes, this is essentially just setting the server-side default to "gen_random_uuid()".)

Note this will only work if you have installed the pgcrypto extension in your postgres instance. If the user you connect with has the right privileges, this can be done by calling the fastapi_utils.guid_type.setup_guids_postgresql function:

import sqlalchemy as sa

from fastapi_utils.guid_type import setup_guids_postgresql

database_uri = "postgresql://user:password@db:5432/app"
engine = sa.create_engine(database_uri)
setup_guids_postgresql(engine)

Non-Server Default

If you are comfortable having no server default for your primary key column, you can still make use of an application-side default (so that sqlalchemy will generate a default value when you create new records):

import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base

from fastapi_utils.guid_type import GUID, GUID_DEFAULT_SQLITE

Base = declarative_base()


class User(Base):
    __tablename__ = "user"
    id = sa.Column(GUID, primary_key=True, default=GUID_DEFAULT_SQLITE)
    name = sa.Column(sa.String, nullable=False)
    related_id = sa.Column(GUID)

GUID_DEFAULT_SQLITE is just an alias for the standard library uuid.uuid4, which could be used in its place.