A Django field that automatically generates formatted IDs with concurrency-safe sequence management.
- Auto-incrementing formatted IDs: Generate IDs like
INV-2024-00001,PO-12345, or any custom format - Concurrency-safe: Uses database row-level locking to prevent duplicate IDs
- Flexible formatting: Support for custom format strings with placeholders
- Dynamic placeholders: Use callables (lambdas) for dynamic values like current year
- Custom starting values: Start sequences at any number
- Manual override support: Optionally set values manually when needed
- Thread and process safe: Works correctly in multi-threaded/multi-process environments
pip install django-formatted-autofieldAdd formatted_autofield to your INSTALLED_APPS:
INSTALLED_APPS = [
...
'formatted_autofield',
...
]Run migrations to create the sequence table:
python manage.py migrate formatted_autofield- Python: 3.10–3.13 (
>=3.10, exactly 3.10 through 3.13 in classifiers) - Django: 5.2.x (
django>=5.2,<5.3) - Databases:
- PostgreSQL is the CI-covered backend (including strict concurrency test coverage)
- SQLite is supported for development and single-instance workflows, with reduced concurrency behavior
from django.db import models
from datetime import datetime
from formatted_autofield import FormattedAutoField
class PurchaseOrder(models.Model):
order_number = FormattedAutoField(
format_string="PO-{year}-{seq:05d}",
placeholders={
"year": lambda: datetime.now().year
},
primary_key=True
)
vendor = models.CharField(max_length=100)
total = models.DecimalField(max_digits=10, decimal_places=2)
# Create instances
po1 = PurchaseOrder.objects.create(vendor="Acme Corp", total=1500.00)
print(po1.order_number) # Output: PO-2026-00001
po2 = PurchaseOrder.objects.create(vendor="Widget Inc", total=2500.00)
print(po2.order_number) # Output: PO-2026-00002class Order(models.Model):
order_id = FormattedAutoField(
format_string="ORD-{seq:05d}",
primary_key=True
)Creates: ORD-00001, ORD-00002, ORD-00003, ...
class Invoice(models.Model):
invoice_number = FormattedAutoField(
format_string="INV-{seq}",
start_at=1000,
primary_key=True
)Creates: INV-1000, INV-1001, INV-1002, ...
from datetime import datetime
class Ticket(models.Model):
ticket_number = FormattedAutoField(
format_string="{year}-{month}-{seq:04d}",
placeholders={
"year": lambda: datetime.now().year,
"month": lambda: datetime.now().strftime("%m")
},
primary_key=True
)Creates: 2026-02-0001, 2026-02-0002, ...
class Product(models.Model):
sku = FormattedAutoField(
format_string="{category}-{seq:06d}",
placeholders={
"category": "WIDGET"
},
max_length=50
)
name = models.CharField(max_length=200)Creates: WIDGET-000001, WIDGET-000002, ...
You can manually set the field value before saving to override auto-generation:
# Auto-generated
order1 = Order.objects.create() # Gets ORD-00001
# Manual override
order2 = Order(order_id="ORD-SPECIAL")
order2.save() # Keeps ORD-SPECIAL
# Back to auto-generated
order3 = Order.objects.create() # Gets ORD-00002Python format string with placeholders. Must include {seq} for the sequence number.
- Use format specifications for padding:
{seq:05d}(5 digits, zero-padded) - Combine with other text:
"PREFIX-{seq}-SUFFIX" - Use multiple placeholders:
"{year}/{month}/{seq:04d}"
Examples:
format_string="ID-{seq}" # ID-1, ID-2, ID-3
format_string="{seq:04d}" # 0001, 0002, 0003
format_string="INV-{year}-{seq}" # INV-2026-1, INV-2026-2Dictionary mapping placeholder names to values or callables.
- Static values:
{"prefix": "ABC"} - Callables:
{"year": lambda: datetime.now().year} - Callables are evaluated at generation time (not at field definition)
Example:
placeholders={
"dept": "SALES",
"year": lambda: datetime.now().year,
"user": lambda: get_current_user().username
}The first number in the sequence.
start_at=1 # Default: 1, 2, 3, ...
start_at=100 # Starts at: 100, 101, 102, ...
start_at=1000 # Starts at: 1000, 1001, 1002, ...Maximum length of the formatted string (CharField limitation).
max_length=50 # For shorter IDs
max_length=200 # For longer formatted stringsEach FormattedAutoField maintains its own sequence counter in the database. The sequence is identified by:
app_label.model_name.field_name
For example: myapp.purchaseorder.order_number
The library uses Django's select_for_update() with transaction.atomic() to ensure thread-safety:
with transaction.atomic():
sequence = Sequence.objects.select_for_update().get_or_create(key=field_key)
sequence.last_value += 1
sequence.save()
next_value = sequence.last_valueThis provides database-level row locking, ensuring no duplicate IDs even with:
- Multiple Django processes (Gunicorn workers, Celery workers)
- Multiple threads
- High-concurrency scenarios
- On INSERT: Values are generated when creating new records
- On UPDATE: Existing values are preserved (not regenerated)
- Manual override: If you set a value before saving, auto-generation is skipped
- PostgreSQL ✅
- MySQL / MariaDB ✅
- Oracle ✅
- SQLite
⚠️ - Works, but uses database-level locking (less concurrent)
The library includes comprehensive tests covering:
- Basic sequencing
- Custom placeholders (static and callable)
- Format string validation
- Concurrency (10+ simultaneous threads)
- Manual overrides
- Update operations
Run tests:
python manage.py test testsDjango cannot serialize lambda functions in migrations. When you define a field with callable placeholders:
order_number = FormattedAutoField(
format_string="PO-{year}-{seq:05d}",
placeholders={"year": lambda: datetime.now().year}
)The lambda will NOT be included in the migration file. You must ensure the field definition with the callable remains in your model file. This is intentional and safe - the callable is evaluated at runtime, not migration time.
If you're adding FormattedAutoField to an existing model with data:
- Add the field as non-primary, nullable first:
legacy_id = models.IntegerField(primary_key=True) # Existing
order_number = FormattedAutoField(
format_string="ORD-{seq:05d}",
null=True, # Temporarily nullable
blank=True
)- Run a data migration to populate values
- Make it the primary key in a subsequent migration if desired
Each ID generation requires:
- One database transaction
- One row lock (SELECT FOR UPDATE)
- One update operation
For high-throughput scenarios:
- Consider bulk creation patterns where possible
- Use database connection pooling
- Ensure proper indexing (automatically created)
The Sequence table has one row per unique field. Even with thousands of models, this table remains small and fast.
class FormattedAutoField(models.CharField):
def __init__(
self,
format_string="{seq}",
placeholders=None,
start_at=1,
*args,
**kwargs
):
...Inherits from: django.db.models.CharField
Automatic settings:
blank=True- Always set (value is auto-generated)editable=False- Field not shown in forms
Internal model for tracking sequence values. Generally, you don't need to interact with this directly.
Fields:
key(CharField): Unique identifier for the sequencelast_value(PositiveIntegerField): Last number issuedcreated_at(DateTimeField): When sequence was createdupdated_at(DateTimeField): Last increment time
Use a year placeholder to include the current year in the rendered identifier:
class Invoice(models.Model):
invoice_number = FormattedAutoField(
format_string="{year}-{seq:05d}",
placeholders={"year": lambda: datetime.now().year}
)Placeholder values affect only the rendered identifier. Each FormattedAutoField keeps one counter for its app/model/field identity. The numeric sequence continues when the callable year changes, for example from 2026-00002 to 2027-00003.
class Request(models.Model):
department = models.CharField(max_length=50)
request_id = FormattedAutoField(
format_string="{dept}-{seq:04d}",
placeholders={"dept": lambda: get_current_department()}
)class Order(models.Model):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
order_number = FormattedAutoField(
format_string="{tenant_code}-{seq:06d}",
placeholders={"tenant_code": lambda: get_current_tenant().code}
)Cause: Django tries to serialize callable placeholders Solution: This is expected. The lambda will be excluded from migrations automatically. Keep the field definition with the lambda in your model file.
Cause: Not using database with row-level locking support Solution: Use PostgreSQL, MySQL, or Oracle for production. SQLite is only recommended for development.
Cause: Manual overrides or deleted records Solution: This is normal behavior. Sequences are monotonically increasing but not necessarily contiguous.
Cause: Sequence already exists from a previous migration/test Solution: Delete the sequence record or manually set its value:
from formatted_autofield.models import Sequence
Sequence.objects.filter(key='myapp.mymodel.myfield').delete()Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
MIT License - see LICENSE file for details.
- GitHub: https://github.com/Perpay/django-formatted-autofield
- PyPI: https://pypi.org/project/django-formatted-autofield/
- Issues: https://github.com/Perpay/django-formatted-autofield/issues
- Consolidate package version source in
formatted_autofield._versionand consume it from both distribution metadata and runtime attribute. - Set supported versions to Python 3.10–3.13 and Django 5.2 (
django>=5.2,<5.3). - Keep PostgreSQL as the supported concurrency-verified backend in CI.
- Added Django 4.1 support in documented release notes.
- Initial release
- FormattedAutoField with custom format strings
- Concurrency-safe sequence management
- Support for static and callable placeholders
- Django 4.2 LTS support
- Comprehensive test suite