How to Handle Timezone-Aware Datetime Properties in Neomodel
Neomodel provides three distinct property types—DateTimeProperty, DateTimeNeo4jFormatProperty, and DateTimeFormatProperty—that handle timezone-aware datetimes by either converting to UTC epoch storage, preserving native Neo4j timezone metadata, or using custom string formats without conversion.
Managing timezone-aware datetime properties in graph databases requires careful handling to prevent silent data corruption or offset errors. The neomodel library for Neo4j offers specialized property implementations that control exactly how Python datetime objects are serialized to the database and whether timezone information is normalized or retained. Understanding these mechanisms ensures your temporal data maintains accuracy across database transactions.
Understanding Neomodel Datetime Property Types
Neomodel implements three property classes in neomodel/properties.py that handle datetime serialization differently. Each serves specific use cases regarding timezone preservation and storage format.
DateTimeProperty (UTC Epoch Storage)
The DateTimeProperty class stores datetimes as Unix epoch timestamps (floating-point seconds). This property always normalizes timezone-aware datetimes to UTC before storage.
According to the source code in neomodel/properties.py (lines 86-105), the inflate method converts stored epochs back to timezone-aware UTC datetimes:
epoch = float(value)
return datetime.fromtimestamp(epoch, tz=ZoneInfo("UTC"))
When deflating (writing to the database), the implementation (lines 119-131) handles timezone conversion based on the force_timezone configuration:
if value.tzinfo:
value = value.astimezone(ZoneInfo("UTC"))
epoch_date = datetime(1970, 1, 1, tzinfo=ZoneInfo("UTC"))
elif get_config().force_timezone:
raise ValueError(f"Error deflating {value}: No timezone provided.")
else:
epoch_date = datetime(1970, 1, 1) # assume UTC
return float((value - epoch_date).total_seconds())
DateTimeNeo4jFormatProperty (Native Timezone Preservation)
The DateTimeNeo4jFormatProperty preserves the original timezone information by utilizing Neo4j's native neo4j.time.DateTime type. Unlike DateTimeProperty, this implementation does not normalize to UTC.
As implemented in neomodel/properties.py (lines 134-144):
def inflate(self, value):
return value.to_native() # Neo4j → datetime with tz
def deflate(self, value):
return neo4j.time.DateTime.from_native(value) # datetime → Neo4j native type
This property type is essential when you need to maintain the original timezone offset (e.g., "America/Los_Angeles") rather than converting to UTC.
DateTimeFormatProperty (Custom String Formatting)
The DateTimeFormatProperty stores datetimes as strings using a user-defined format string (e.g., %Y-%m-%d %H:%M:%S). This property performs no automatic timezone conversion—the string is stored exactly as formatted.
Use this property when you need human-readable date strings in the database or when integrating with systems that expect specific string formats.
Configuring Strict Timezone Validation with force_timezone
Neomodel provides a global configuration flag force_timezone that controls how DateTimeProperty handles naive (timezone-unaware) datetimes.
Defined in neomodel/config.py (lines 22-30):
force_timezone: bool = field(
default=False,
metadata={
"env_var": "NEOMODEL_FORCE_TIMEZONE",
"description": "Force timezone‑aware datetime objects",
},
)
When force_timezone is set to True, neomodel rejects any naive datetime objects during deflation, raising a ValueError. This enforces explicit timezone handling in your application code.
Enable strict validation programmatically:
from neomodel import config
config.force_timezone = True
Or via environment variable:
export NEOMODEL_FORCE_TIMEZONE=1
Practical Implementation Examples
Basic UTC Storage with DateTimeProperty
Use DateTimeProperty when you want consistent UTC storage and automatic conversion:
from neomodel import StructuredNode, DateTimeProperty
from datetime import datetime, timezone
class Event(StructuredNode):
# Automatically stores current UTC time when node is created
created_at = DateTimeProperty(default_now=True)
# Create a node – created_at is stored as float epoch in UTC
e = Event().save()
print(e.created_at) # → 2026-03-08 12:34:56+00:00 (UTC)
Enforcing Timezone-Aware Datetimes
Prevent silent errors by requiring explicit timezones:
from neomodel import StructuredNode, DateTimeProperty, config
from datetime import datetime, timezone
# Enable strict timezone validation
config.force_timezone = True
class LogEntry(StructuredNode):
timestamp = DateTimeProperty()
# ✅ Correct: timezone-aware datetime
log = LogEntry(timestamp=datetime(2026, 3, 8, 15, 0, tzinfo=timezone.utc)).save()
# ❌ Raises ValueError: "Error deflating 2026-03-08 15:00:00: No timezone provided."
LogEntry(timestamp=datetime(2026, 3, 8, 15, 0)).save()
Preserving Original Timezones
Maintain the original timezone offset using DateTimeNeo4jFormatProperty:
from neomodel import StructuredNode, DateTimeNeo4jFormatProperty
from datetime import datetime, timezone, timedelta
class Meeting(StructuredNode):
# Preserves the specific timezone offset in Neo4j's native format
start_time = DateTimeNeo4jFormatProperty()
# Create with Pacific Time (UTC-8)
pacific = timezone(timedelta(hours=-8))
meeting = Meeting(
start_time=datetime(2026, 3, 8, 9, 0, tzinfo=pacific)
).save()
# Retrieved datetime maintains the original -08:00 offset
print(meeting.start_time) # → 2026-03-08 09:00:00-08:00
Custom String Formatting
Store datetimes as formatted strings without timezone conversion:
from neomodel import StructuredNode, DateTimeFormatProperty
from datetime import datetime
class Appointment(StructuredNode):
# Stores as "2026-03-08 14:30" (no timezone conversion)
scheduled_for = DateTimeFormatProperty(format="%Y-%m-%d %H:%M")
appointment = Appointment(scheduled_for=datetime(2026, 3, 8, 14, 30)).save()
# Neo4j stores the literal string: "2026-03-08 14:30"
Summary
DateTimePropertyconverts all datetimes to UTC epoch floats for storage, with optional strict timezone validation via theforce_timezoneconfiguration flag.DateTimeNeo4jFormatPropertypreserves original timezone offsets using Neo4j's nativeDateTimetype, avoiding UTC normalization.DateTimeFormatPropertystores datetimes as custom-formatted strings without automatic timezone conversion.- Enable
config.force_timezone = Trueto enforce explicit timezone handling and prevent silent assumptions about UTC for naive datetimes.
Frequently Asked Questions
What happens if I store a naive datetime using DateTimeProperty?
By default, DateTimeProperty assumes naive datetimes are in UTC and converts them to epoch timestamps accordingly. However, if you set config.force_timezone = True, neomodel raises a ValueError requiring you to provide timezone-aware datetime objects explicitly.
Which property type should I use to preserve the original timezone offset?
Use DateTimeNeo4jFormatProperty when you need to maintain the original timezone information (such as "America/New_York" or UTC-05:00). This property utilizes Neo4j's native DateTime type, which stores the timezone offset alongside the timestamp, unlike DateTimeProperty which normalizes everything to UTC.
How do I enable strict timezone validation across my entire application?
Set the global configuration flag before defining your node classes:
from neomodel import config
config.force_timezone = True
Alternatively, set the environment variable NEOMODEL_FORCE_TIMEZONE=1 before starting your application. This configuration affects all DateTimeProperty instances, ensuring they only accept timezone-aware datetime objects.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →