SQLAlchemy
DateTime
Python
Database
ORM

SQLAlchemy default DateTime

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

SQLAlchemy provides two ways to set default datetime values on columns: default (sets the value in Python when creating the object) and server_default (adds a DEFAULT clause in the SQL DDL so the database generates the value). For "created at" timestamps, use server_default=func.now() so the database clock is authoritative. For "updated at" timestamps, use onupdate=func.now() which sets the value on every UPDATE. A common mistake is using default=datetime.utcnow() with parentheses, which evaluates once at import time and gives every row the same timestamp.

The Classic Mistake

python
1from datetime import datetime
2from sqlalchemy import Column, DateTime, Integer, String
3from sqlalchemy.orm import declarative_base
4
5Base = declarative_base()
6
7class User(Base):
8    __tablename__ = "users"
9    id = Column(Integer, primary_key=True)
10    name = Column(String(100))
11
12    # WRONG: datetime.utcnow() is called ONCE at import time
13    created_at = Column(DateTime, default=datetime.utcnow())
14    # Every row gets the same timestamp — the time the module was imported
15
16    # CORRECT: pass the function without parentheses
17    created_at = Column(DateTime, default=datetime.utcnow)
18    # datetime.utcnow is called each time a new row is inserted

default=datetime.utcnow (no parentheses) passes the function as a callable. SQLAlchemy calls it each time a new object is created. default=datetime.utcnow() (with parentheses) evaluates immediately and stores the resulting fixed timestamp.

Python-Side Default (default)

python
1from datetime import datetime, timezone
2from sqlalchemy import Column, DateTime, Integer, String
3from sqlalchemy.orm import declarative_base
4
5Base = declarative_base()
6
7class Article(Base):
8    __tablename__ = "articles"
9    id = Column(Integer, primary_key=True)
10    title = Column(String(200))
11
12    # Called in Python when the object is instantiated
13    created_at = Column(DateTime, default=datetime.utcnow)
14
15    # With timezone-aware datetime (recommended)
16    created_at = Column(
17        DateTime(timezone=True),
18        default=lambda: datetime.now(timezone.utc)
19    )
20
21# Usage
22article = Article(title="Hello World")
23print(article.created_at)  # Set immediately in Python, before flush

default is a Python-side feature. The value is set when the ORM creates the object, before it is sent to the database. The database column has no DEFAULT clause.

Server-Side Default (server_default)

python
1from sqlalchemy import Column, DateTime, Integer, String, func, text
2from sqlalchemy.orm import declarative_base
3
4Base = declarative_base()
5
6class Article(Base):
7    __tablename__ = "articles"
8    id = Column(Integer, primary_key=True)
9    title = Column(String(200))
10
11    # Database generates the timestamp — adds DEFAULT to DDL
12    created_at = Column(DateTime, server_default=func.now())
13    # DDL: created_at DATETIME DEFAULT CURRENT_TIMESTAMP
14
15    # Or use a text expression
16    created_at = Column(DateTime, server_default=text("CURRENT_TIMESTAMP"))
17
18    # With timezone
19    created_at = Column(
20        DateTime(timezone=True),
21        server_default=func.now()
22    )

server_default adds a DEFAULT clause to the column in the database schema. The value is generated by the database server, ensuring consistency even for raw SQL inserts outside SQLAlchemy.

Updated At with onupdate

python
1from datetime import datetime, timezone
2from sqlalchemy import Column, DateTime, Integer, String, func
3from sqlalchemy.orm import declarative_base
4
5Base = declarative_base()
6
7class Article(Base):
8    __tablename__ = "articles"
9    id = Column(Integer, primary_key=True)
10    title = Column(String(200))
11
12    # Set on creation
13    created_at = Column(
14        DateTime(timezone=True),
15        server_default=func.now()
16    )
17
18    # Set on creation AND updated on every modification
19    updated_at = Column(
20        DateTime(timezone=True),
21        server_default=func.now(),
22        onupdate=func.now()
23    )
24
25# Usage
26article = Article(title="Hello")
27session.add(article)
28session.commit()
29print(article.created_at)  # 2025-01-15 10:00:00
30print(article.updated_at)  # 2025-01-15 10:00:00
31
32article.title = "Updated Title"
33session.commit()
34print(article.updated_at)  # 2025-01-15 11:30:00 (changed)
35print(article.created_at)  # 2025-01-15 10:00:00 (unchanged)

onupdate calls the function every time an UPDATE statement is issued for the row via SQLAlchemy. It is Python-side — the database does not enforce it.

Server-Side onupdate (MySQL)

python
1from sqlalchemy import Column, DateTime, Integer, String, text
2from sqlalchemy.orm import declarative_base
3
4Base = declarative_base()
5
6class Article(Base):
7    __tablename__ = "articles"
8    id = Column(Integer, primary_key=True)
9    title = Column(String(200))
10
11    created_at = Column(
12        DateTime,
13        server_default=text("CURRENT_TIMESTAMP")
14    )
15
16    # MySQL-specific: ON UPDATE CURRENT_TIMESTAMP
17    updated_at = Column(
18        DateTime,
19        server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")
20    )

MySQL supports ON UPDATE CURRENT_TIMESTAMP natively. This works for all updates, including raw SQL outside SQLAlchemy.

SQLAlchemy 2.0 Style (Mapped Columns)

python
1from datetime import datetime
2from sqlalchemy import func
3from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
4
5class Base(DeclarativeBase):
6    pass
7
8class Article(Base):
9    __tablename__ = "articles"
10    id: Mapped[int] = mapped_column(primary_key=True)
11    title: Mapped[str] = mapped_column()
12
13    created_at: Mapped[datetime] = mapped_column(
14        server_default=func.now()
15    )
16    updated_at: Mapped[datetime] = mapped_column(
17        server_default=func.now(),
18        onupdate=func.now()
19    )

SQLAlchemy 2.0 uses Mapped type annotations and mapped_column() instead of Column(). The default, server_default, and onupdate parameters work the same way.

Common Pitfalls

  • Using default=datetime.utcnow() with parentheses: This evaluates utcnow() once at class definition time. Every row gets the same timestamp. Remove the parentheses: default=datetime.utcnow passes the function itself, which is called for each new row.
  • Using default instead of server_default for timestamps: default only works for inserts made through SQLAlchemy. Direct SQL inserts, database migrations, or other applications inserting into the same table will get NULL. Use server_default=func.now() so the database sets the value.
  • Expecting onupdate to work for raw SQL updates: onupdate is a Python-side feature that only triggers when SQLAlchemy's ORM issues an UPDATE. Raw SQL, bulk updates, or other applications bypassing SQLAlchemy will not trigger it. Use database triggers or MySQL's ON UPDATE CURRENT_TIMESTAMP for database-enforced behavior.
  • Using datetime.utcnow (deprecated in Python 3.12): datetime.utcnow() returns a naive datetime and is deprecated. Use datetime.now(timezone.utc) for timezone-aware UTC timestamps: default=lambda: datetime.now(timezone.utc).
  • Not using DateTime(timezone=True) for timezone-aware columns: Without timezone=True, the column stores naive datetimes. If different parts of your application assume different timezones, this causes silent data corruption. Always store timezone-aware timestamps.

Summary

  • Use default=datetime.utcnow (no parentheses) for Python-side defaults called per insert
  • Use server_default=func.now() for database-enforced defaults that work with raw SQL
  • Use onupdate=func.now() for "updated at" timestamps via SQLAlchemy ORM
  • Never use default=datetime.utcnow() with parentheses — it evaluates once at import time
  • Use DateTime(timezone=True) and datetime.now(timezone.utc) for timezone-aware timestamps
  • For database-enforced "updated at", use MySQL's ON UPDATE CURRENT_TIMESTAMP or database triggers

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.