django-identifiers
Short, collision-resistant identifiers for Django with automatic generation, immutability, and bulk-create support.
django-identifiers provides a small, focused identifier system for Django
models.
It handles two workflows that need different treatment:
- normal Django model saves, where
Model.save()can generate identifiers automatically; and - bulk insertion, where
bulk_create()bypasses model save hooks and identifiers must be assigned before the insert.
The package keeps the database unique constraint as the final authority for uniqueness, while providing generation, best-effort collision avoidance, retry behavior, immutable identifier fields, and bulk-safe helpers.
- PyPI: https://pypi.org/project/django-identifiers/
- Source: https://github.com/fifoa-labs/django-identifiers
- License: MIT
Identifier policy
│
▼
Generate candidate
│
▼
Best-effort DB check
│
▼
Database write
│
├── success ───────────────► done
│
└── IntegrityError
│
▼
regenerate / retry
The package solves identifier generation and lifecycle behavior.
Your application decides which models need identifiers, which fields are managed, which patterns they use, and whether those identifiers may change.
Why django-identifiers?
Short application identifiers appear everywhere:
- public codes
- order references
- opaque record identifiers
- SKUs
- account handles
- import references
- URL-safe record codes
- support references
- compact internal identifiers
Generating a random string is easy.
Making identifier behavior consistent across Django admin, APIs, scripts, factories, background jobs, normal saves, and bulk imports is where the problem becomes more subtle.
Common problems include:
- duplicate generation logic across models
- assuming a pre-insert
.exists()check guarantees uniqueness - forgetting that
bulk_create()skipssave() - silently allowing identifiers to be edited after creation
- inconsistent character sets between models
- retry behavior implemented differently in every importer
- project-specific registries becoming tightly coupled to model names
- models with multiple identifiers being forced into one global pattern
- identifiers that are technically unique but awkward to read or type
django-identifiers provides one reusable Django-focused foundation for those
concerns.
It is intentionally small.
It does not try to become a UUID, ULID, slug, natural-key, sequence, or primary key framework.
Core Principles
The database is the source of truth
A generated identifier may be checked before insertion:
Model._default_manager.filter(code=candidate).exists()
That check is useful because it avoids many ordinary collisions.
It is not a concurrency guarantee.
Another worker can insert the same value after the check and before the current transaction writes its row.
The only authoritative cross-process uniqueness guarantee is the database constraint:
code = models.CharField(
max_length=64,
unique=True,
blank=True,
)
django-identifiers is designed around that fact.
Normal saves and bulk inserts are different workflows
Normal model creation:
obj.save()
runs model save behavior.
Bulk creation:
Model.objects.bulk_create(objects)
does not call each object's save() method.
AutoIdentifiersMixin therefore handles normal model lifecycle creation, while
the bulk helpers handle pre-generation and resilient bulk insertion.
Identifier policy belongs to the model
The package does not ship a global registry containing project model names.
Each consuming model declares its own identifier policy:
AUTO_IDENTIFIERS = {
"code": {
"pattern": "aaaaaaaaa",
"immutable": True,
},
}
A model may manage more than one identifier field, with a different policy for each field.
Features
- Automatic identifier generation on first Django model save
- Multiple managed identifier fields per model
- Per-field generation patterns
- Per-field fallback lengths
- Optional identifier immutability
- Explicit escape hatches for controlled identifier changes
- Best-effort database collision checks
- Database-backed uniqueness as the final authority
- Retry behavior after
IntegrityError - Safe helper for script/factory single-object creation
- Batch identifier generation
- Guaranteed uniqueness within a generated batch
- Optional filtering of identifiers already present in the database
- In-memory assignment for objects destined for
bulk_create() - Resilient bulk creation with collision retry
- Recursive splitting of persistently failing bulk batches
- Django default-manager support
- String model references such as
"orders.Order" - Pattern literals and readable character sets
- Numeric-only identifier generation
- Fully typed package with
py.typed - Strict mypy validation
- 100% statement and branch coverage
- Clean-wheel installation validation
Installation
Install from PyPI:
python -m pip install django-identifiers
With uv:
uv add django-identifiers
The package does not own database tables and does not ship migrations.
You do not add django_identifiers to INSTALLED_APPS.
There are no package settings required.
You only import and use the APIs you need.
Quick Start
The most common use case is an automatically generated identifier that should never change after creation.
from django.db import models
from django_identifiers import AutoIdentifiersMixin
class Product(AutoIdentifiersMixin, models.Model):
name = models.CharField(max_length=255)
code = models.CharField(
max_length=64,
unique=True,
blank=True,
)
AUTO_IDENTIFIERS = {
"code": {
"pattern": "aaaaaaaaa",
"immutable": True,
},
}
Create normally:
product = Product(name="Milk")
product.save()
print(product.code)
The blank code field is populated during the first save.
An identifier using:
aaaaaaaaa
contains nine characters chosen from the package's safe lowercase alphanumeric alphabet.
If a value is supplied explicitly, the mixin preserves it:
product = Product(
name="Milk",
code="legacy42",
)
product.save()
assert product.code == "legacy42"
The mixin only auto-generates fields that are blank.
Recommended Model Field
For most generated identifiers:
code = models.CharField(
max_length=64,
unique=True,
blank=True,
)
Important attributes:
unique=True
Strongly recommended.
The database unique constraint is the final authority for uniqueness and is required for collision safety across concurrent writers.
blank=True
Allows the field to remain blank before the mixin generates its value.
editable=False
Optional.
Use it when administrators or forms should never manually assign the field:
code = models.CharField(
max_length=64,
unique=True,
blank=True,
editable=False,
)
Whether a field is editable in a form and whether it is immutable after creation are separate decisions.
Pattern Syntax
Patterns describe how an identifier should be generated.
Supported tokens:
| Token | Meaning |
|---|---|
L |
uppercase letter |
l |
lowercase letter |
N |
digit 2-9 |
a |
lowercase letter or digit 2-9 |
A |
uppercase letter or digit 2-9 |
The package excludes visually ambiguous characters from generated alphabets.
Uppercase letters exclude:
I L O
Lowercase letters exclude:
i l o
Numeric generation excludes:
0 1
Examples
"NNNNNN"
Example shape:
284735
"LNLNLNNNN"
Example shape:
A3B7C9284
"aaaaaaaaa"
Example shape:
r7m2q8v4c
"ORD-NNNNNN"
Example shape:
ORD-734829
Any character that is not a recognized token is emitted literally.
That means prefixes, separators, and other fixed characters may be embedded directly in the pattern.
Pattern Length
When a pattern is supplied, the effective output length is the length of the pattern itself.
For example:
AUTO_IDENTIFIERS = {
"code": {
"pattern": "ORD-NNNNNN",
},
}
always produces an identifier with the same total length as:
ORD-NNNNNN
A separate length value is only relevant when no pattern is being used.
Configuring Multiple Identifier Fields
A model may manage multiple fields independently.
from django.db import models
from django_identifiers import AutoIdentifiersMixin
class Order(AutoIdentifiersMixin, models.Model):
sku = models.CharField(
max_length=64,
unique=True,
blank=True,
)
code = models.CharField(
max_length=64,
unique=True,
blank=True,
)
AUTO_IDENTIFIERS = {
"sku": {
"pattern": "LL-NNNNNN",
"immutable": True,
},
"code": {
"pattern": "aaaaaaaaa",
"immutable": False,
},
}
The two fields do not need to share the same pattern or immutability policy.
This is useful when one model needs, for example:
- a permanent external reference; and
- a mutable application-facing handle.
Automatic Generation with AutoIdentifiersMixin
AutoIdentifiersMixin is an abstract Django model mixin.
Use it before models.Model or another concrete Django model base:
class Product(AutoIdentifiersMixin, models.Model):
...
The mixin detects creation using Django's model state:
self._state.adding
This is more reliable than checking whether pk is None, because some models
may receive primary keys before their first database save.
Creation behavior
On the first save:
- the mixin reads
AUTO_IDENTIFIERS; - it identifies managed fields whose current value is blank;
- it generates a value for each blank managed field;
- it attempts the save inside
transaction.atomic(); - if an
IntegrityErroroccurs, generated fields are regenerated and the save is retried; - after the configured retry count, a final save is attempted and any
persistent
IntegrityErroris allowed to propagate.
Explicitly supplied identifier values are never replaced automatically.
Retry count
The default create retry count is:
AUTO_IDENTIFIER_MAX_RETRIES = 3
Override it on a model when needed:
class HighTrafficModel(AutoIdentifiersMixin, models.Model):
AUTO_IDENTIFIER_MAX_RETRIES = 5
# ...
Retries are a collision-recovery mechanism.
If collisions become common, increase the identifier keyspace rather than relying on large retry counts.
Identifier Immutability
Identifiers may be marked immutable:
AUTO_IDENTIFIERS = {
"code": {
"pattern": "aaaaaaaaa",
"immutable": True,
},
}
After creation:
obj.code = "different"
obj.save()
raises:
ValueError
The database value remains unchanged.
This protects stable application references from accidental edits.
Allow one intentional change
A controlled operation may explicitly allow a specific identifier field to change:
obj.code = "replacement"
obj.save(_allow_code_change=True)
The escape-hatch keyword follows this pattern:
_allow_<field_name>_change
Examples:
_allow_code_change=True
_allow_sku_change=True
_allow_reference_change=True
Allow all identifier changes for one save
A controlled maintenance operation may allow every immutable identifier on the model to change:
obj.save(_allow_identifier_change=True)
These flags apply only to that save call.
They are deliberately explicit so ordinary model updates cannot silently mutate stable identifiers.
Direct Identifier Generation
You do not have to use the model mixin.
generate_identifier()
Generate an identifier without a model:
from django_identifiers import generate_identifier
code = generate_identifier(
pattern="LNLNLNNNN",
)
Or generate using a simple length:
code = generate_identifier(
length=10,
)
When no pattern is supplied, the default generator uses safe uppercase letters and digits.
Generate for a model field
code = generate_identifier(
model_class=Product,
field_name="code",
)
When a model is supplied:
- the field's
AUTO_IDENTIFIERSconfiguration is considered; - candidates are generated using the resolved policy;
- existing database values are checked before a candidate is returned.
The existence check reduces ordinary collisions but does not replace the database unique constraint.
String model references
A model may also be referenced using Django's normal app-label syntax:
code = generate_identifier(
model_class="orders.Order",
field_name="code",
)
This is resolved through Django's application registry.
Use this only after Django has been initialized.
Random String Generation
The low-level generator is public:
from django_identifiers import generate_random_string
Pattern-based:
value = generate_random_string("LL-NNNN")
Length-based:
value = generate_random_string(length=12)
This helper does not query the database.
Use it when you only need generation mechanics and do not need model-aware collision checking.
Numeric Identifiers
generate_random_number()
Generate a numeric string using digits 2-9:
from django_identifiers import generate_random_number
number = generate_random_number(8)
Example:
78245329
The value is returned as a string.
That preserves leading-width semantics and avoids converting an application identifier into arithmetic data.
generate_number()
Generate a numeric identifier with optional model-aware collision checking:
from django_identifiers import generate_number
number = generate_number(
length=8,
model_class=Invoice,
field_name="reference_number",
)
Without a model:
number = generate_number(length=8)
Both numeric helpers exclude 0 and 1.
Single-Object Script and Factory Creation
Use safe_generate_identifier() when a script, factory, command, or job owns
the actual save operation and is not relying on AutoIdentifiersMixin.
from django_identifiers import safe_generate_identifier
order = Order(
customer=customer,
)
code = safe_generate_identifier(
order,
field_name="code",
)
The helper:
- generates a candidate;
- assigns it to the instance;
- saves inside
transaction.atomic(); - retries when an
IntegrityErroroccurs; - returns the successfully saved identifier.
The default attempt count is:
max_attempts=5
Customize it:
safe_generate_identifier(
order,
field_name="code",
max_attempts=8,
)
If all attempts fail, the helper raises:
IdentifierGenerationError
When not to use it
If the model already uses AutoIdentifiersMixin and the field is configured in
AUTO_IDENTIFIERS, a normal:
obj.save()
already owns automatic generation and retry behavior.
Do not wrap every normal save in safe_generate_identifier() unnecessarily.
Bulk Creation
Bulk creation is a major reason this package exists.
Django's:
Model.objects.bulk_create(objects)
does not call each object's save() method.
Therefore:
AutoIdentifiersMixin
does not run for those rows.
For bulk workflows, use the package's bulk APIs.
Recommended Bulk Workflow
The simplest complete workflow is:
from django_identifiers import bulk_create_with_identifiers
objects = [
Product(name="Milk"),
Product(name="Bread"),
Product(name="Coffee"),
]
created = bulk_create_with_identifiers(
model_class=Product,
instances=objects,
field_name="code",
)
The helper:
- finds objects whose target identifier field is blank;
- generates unique candidates in memory;
- optionally filters candidates already present in the database;
- inserts objects in batches;
- catches
IntegrityError; - regenerates identifiers for the failed batch;
- retries the insert;
- recursively splits persistently failing multi-row batches;
- ultimately allows a persistent single-row
IntegrityErrorto propagate.
The return value is the number of created objects:
created = bulk_create_with_identifiers(...)
Bulk Batch Size
The default batch size is:
batch_size=1000
Customize it:
bulk_create_with_identifiers(
model_class=Product,
instances=objects,
field_name="code",
batch_size=500,
)
A useful starting range for many applications is:
500-1000
Larger batches reduce insertion overhead but increase the amount of work that must be retried when one row causes the batch to fail.
Choose the size based on:
- database backend
- row width
- expected import volume
- identifier collision probability
- other model constraints
- transaction characteristics
Bulk Retry Behavior
The default retry count per chunk is:
max_retries=5
Override it:
bulk_create_with_identifiers(
model_class=Product,
instances=objects,
max_retries=3,
)
When a bulk insert raises IntegrityError, identifiers in the failed chunk are
regenerated and the entire chunk is retried.
If the configured retry limit is reached and the chunk contains multiple rows, the chunk is split in half and each half is attempted independently.
Conceptually:
1000-row chunk fails
│
▼
regenerate + retry
│
▼
retry limit reached
│
├── 500 rows
│ ├── 250
│ └── 250
│
└── 500 rows
├── 250
└── 250
This allows persistent non-identifier integrity failures to be isolated to smaller groups and ultimately to a single row.
A persistent single-row IntegrityError is not hidden.
It propagates to the caller.
Pre-Assign Identifiers Without Writing
If you want to manage bulk_create() yourself, use
assign_missing_identifiers().
from django_identifiers import assign_missing_identifiers
objects = [
Product(name="Milk"),
Product(name="Bread"),
]
assign_missing_identifiers(
model_class=Product,
instances=objects,
field_name="code",
)
Product.objects.bulk_create(
objects,
batch_size=1000,
)
assign_missing_identifiers() performs no writes.
It only modifies the model instances in memory.
Existing values are preserved:
objects = [
Product(name="Milk", code="existing"),
Product(name="Bread", code=""),
]
After assignment:
Milk → existing
Bread → generated identifier
Concurrency warning
This two-step workflow does not provide collision retry around your own
subsequent bulk_create().
If multiple workers may insert concurrently and you want package-managed recovery, prefer:
bulk_create_with_identifiers(...)
Generate a Batch Without Assigning It
Use generate_identifiers_batch() when you need the generated values directly.
from django_identifiers import generate_identifiers_batch
codes = generate_identifiers_batch(
model_class=Product,
count=1000,
field_name="code",
)
The function guarantees that returned values are unique within that returned batch.
By default it also filters values already present in the database.
Disable database checking
For workflows where no database check is needed:
codes = generate_identifiers_batch(
model_class=Product,
count=1000,
field_name="code",
db_check=False,
)
This removes the best-effort existing-value query.
It does not change the fundamental concurrency rule: only a database unique constraint can globally guarantee uniqueness.
Generation rounds
Batch generation over-generates candidates to absorb duplicates and filters.
The default maximum number of generation rounds is:
max_rounds=10
If the function cannot produce enough unique candidates within that limit, it raises:
IdentifierGenerationError
Repeated exhaustion usually indicates that the configured identifier keyspace is too small for the requested volume.
Concurrency
django-identifiers is designed for multiple writers.
Consider two workers:
Worker A Worker B
-------- --------
generate abc123 generate abc123
DB pre-check: free DB pre-check: free
insert abc123 insert abc123
success unique constraint fails
regenerate
retry
Both workers were correct when they performed their pre-check.
The race occurred afterward.
This is why the database constraint is authoritative.
The package's retry behavior is designed to recover from these collisions.
Recommended concurrent-write model
For normal saves:
AutoIdentifiersMixin
+
unique=True
+
retry on IntegrityError
For bulk writers:
bulk_create_with_identifiers()
+
unique=True
+
retry on IntegrityError
No centralized reservation service is required for ordinary workloads.
Choosing Identifier Length
Collision probability depends on the number of possible identifiers in the configured keyspace.
Longer identifiers provide more space.
Shorter identifiers are easier to display and type.
The right balance depends on table size and write volume.
General guidance:
- very small datasets may comfortably use 6-character identifiers;
- general application identifiers often benefit from 8-10 characters;
- high-volume or long-lived tables should use larger keyspaces;
- heavy concurrent creation benefits from additional keyspace;
- if retries become routine, increase the keyspace.
Do not solve frequent collisions by simply increasing retry counts.
Frequent collisions indicate that generation space is too constrained.
Readability Versus Meaning
django-identifiers generates opaque application identifiers.
The package deliberately avoids visually confusing characters, which improves readability and manual transcription.
That does not make generated identifiers semantically meaningful.
For example:
r7m2q8v4c
is easier to read than an alphabet that freely mixes ambiguous characters, but the value itself carries no business meaning.
If your identifier must encode business information, dates, regions, sequence numbers, or other semantics, design that policy deliberately rather than treating random-generation patterns as an encoding framework.
Identifiers in URLs
Generated identifiers can be useful in URLs:
path(
"products/<str:code>/",
views.product_detail,
name="product-detail",
)
Lookup:
product = get_object_or_404(
Product,
code=code,
)
When using identifiers publicly:
- choose sufficient keyspace;
- keep the database unique constraint;
- do not treat obscurity as authorization;
- continue to enforce normal application permissions;
- do not assume a random-looking identifier is secret.
An identifier is a reference, not an access-control mechanism.
Identifier Configuration Reference
The primary model configuration is:
AUTO_IDENTIFIERS
Example:
AUTO_IDENTIFIERS = {
"code": {
"pattern": "aaaaaaaaa",
"immutable": True,
},
}
Supported field options:
pattern
Example:
"pattern": "LNLNLNNNN"
Defines the generation pattern.
length
Example:
"length": 10
Used when no pattern is configured.
Example:
AUTO_IDENTIFIERS = {
"code": {
"length": 10,
"immutable": True,
},
}
immutable
Example:
"immutable": True
Prevents normal changes to the field after creation.
Default behavior when omitted is mutable:
"immutable": False
Configuration Precedence
When generate_identifier() is used with a model field, per-field model
configuration takes precedence over fallback arguments supplied to the function.
For example:
class Product(AutoIdentifiersMixin, models.Model):
code = models.CharField(
max_length=64,
unique=True,
blank=True,
)
AUTO_IDENTIFIERS = {
"code": {
"pattern": "NNNNNN",
},
}
Then:
generate_identifier(
model_class=Product,
field_name="code",
pattern="LLLLLL",
)
uses the model field's configured:
NNNNNN
policy.
The model owns its declared identifier policy.
Function arguments act as fallbacks.
Database Managers
django-identifiers uses Django's default manager rather than assuming every
model exposes a manager named:
objects
This means custom manager naming is supported as long as Django has a valid default manager for the model.
The package internally works through:
Model._default_manager
Consuming applications do not need to access this private Django attribute themselves.
Transactions
Single-object safe generation and automatic create retries use:
transaction.atomic()
Bulk insertion retries also isolate writes inside atomic blocks.
This is important because a failed database write must be rolled back before a new candidate can be attempted safely.
Your application may still wrap higher-level workflows in its own transactions when appropriate.
IntegrityError Semantics
Identifier collision recovery is triggered by Django's:
IntegrityError
A database may raise IntegrityError for reasons other than identifier
collisions, such as:
- another unique constraint
- a check constraint
- a foreign-key constraint
- another database integrity rule
The package does not attempt to parse backend-specific database error strings to determine which constraint failed.
For single-object retry flows, an unrelated persistent integrity error will continue to fail and eventually propagate or produce generation exhaustion according to the API being used.
For resilient bulk creation, persistently failing chunks are recursively split until a single-row failure can propagate.
This behavior keeps the package backend-independent.
Public Python API
The supported root-package API is:
from django_identifiers import (
AutoIdentifiersMixin,
IdentifierGenerationError,
assign_missing_identifiers,
bulk_create_with_identifiers,
generate_identifier,
generate_identifiers_batch,
generate_number,
generate_random_number,
generate_random_string,
safe_generate_identifier,
)
AutoIdentifiersMixin
Automatic generation and optional immutability for normal Django model saves.
IdentifierGenerationError
Raised when generation cannot produce the required identifier or identifiers within the configured limits.
generate_identifier
Generate a pattern- or length-based identifier, optionally with model-aware database checking.
generate_random_string
Low-level random string generation without database access.
generate_random_number
Low-level numeric string generation using digits 2-9.
generate_number
Numeric identifier generation with optional model-aware database checking.
safe_generate_identifier
Generate, assign, save, and retry a single model instance.
generate_identifiers_batch
Generate many unique identifier candidates efficiently.
assign_missing_identifiers
Assign generated identifiers to blank fields in memory without writing.
bulk_create_with_identifiers
Assign identifiers and perform resilient bulk creation.
Implementation helpers outside the root public API should be treated as internal.
Import Behavior
The root package can be imported without configured Django settings:
import django_identifiers
The Django-model-dependent:
AutoIdentifiersMixin
is exposed lazily.
Normal Django projects may simply write:
from django_identifiers import AutoIdentifiersMixin
after Django has been configured in the usual way.
This keeps package metadata and installation checks importable without requiring an initialized Django application registry.
What django-identifiers Does Not Do
django-identifiers intentionally does not:
- replace Django primary keys
- provide sequential business numbering
- provide database sequences
- provide UUID generation
- provide ULID generation
- replace slugs
- create natural keys
- encode business semantics into identifiers
- provide authorization
- treat identifiers as secrets
- require a specific user model
- require Django REST Framework
- require django-allauth
- require Celery
- require Redis
- require PostgreSQL
- own application models
- ship database migrations
- maintain a project-specific global model registry
For sequential identifiers, use a database sequence or another mechanism designed for ordering.
For user-entered natural identifiers such as email addresses, use the domain model's own validation and uniqueness rules.
When to Use django-identifiers
Good use cases include:
Product.code
Order.code
Document.reference
ImportBatch.code
SupportCase.reference
Account.public_code
Asset.sku
The package is especially useful when:
- the identifier should be generated automatically;
- the value should be shorter than a UUID;
- multiple processes may create rows;
- imports rely on
bulk_create(); - some identifiers should become immutable after creation;
- the same behavior should be shared across multiple Django projects.
When Not to Use django-identifiers
Do not use this package merely because a model needs some unique field.
It is usually not appropriate for:
Primary keys
Django and the database already provide primary-key mechanisms.
Natural keys
Examples:
email address
government-assigned identifier
externally supplied account number
These values come from the domain, not from random generation.
Sequential numbering
If the business requires:
INV-000001
INV-000002
INV-000003
use a sequence-oriented system.
Random collision-resistant identifiers do not provide ordering or gap-free numbering.
Secrets
Identifiers are not credentials.
Do not use them as password-reset tokens, authentication tokens, API secrets, or authorization controls.
Use purpose-built cryptographic token systems for those workflows.
Practical Model Example
"""
orders/models.py
Order models.
"""
from __future__ import annotations
from typing import ClassVar
from django.db import models
from django_identifiers import AutoIdentifiersMixin
class Order(AutoIdentifiersMixin, models.Model):
code = models.CharField(
max_length=32,
unique=True,
blank=True,
editable=False,
)
AUTO_IDENTIFIERS: ClassVar[dict[str, dict[str, object]]] = {
"code": {
"pattern": "ORD-NNNNNNNN",
"immutable": True,
},
}
Normal creation:
order = Order()
order.save()
print(order.code)
Example:
ORD-57283462
The value is generated once and protected from accidental mutation.
Practical Bulk Import Example
Suppose an importer builds 50,000 rows:
objects = [
ImportedRecord(
source_id=row.source_id,
amount=row.amount,
)
for row in source_rows
]
Use:
from django_identifiers import bulk_create_with_identifiers
created = bulk_create_with_identifiers(
model_class=ImportedRecord,
instances=objects,
field_name="code",
batch_size=1000,
max_retries=5,
)
The importer does not need to manually reproduce identifier-generation policy.
The model may own that policy:
AUTO_IDENTIFIERS = {
"code": {
"pattern": "aaaaaaaaaa",
"immutable": True,
},
}
Both normal saves and bulk helpers therefore use the same model-level identifier pattern.
Practical Pre-Assignment Example
Sometimes an import must inspect or serialize generated identifiers before writing.
from django_identifiers import assign_missing_identifiers
assign_missing_identifiers(
model_class=ImportedRecord,
instances=objects,
field_name="code",
)
for obj in objects:
print(obj.code)
ImportedRecord.objects.bulk_create(
objects,
batch_size=1000,
)
Remember that this manual bulk-create pattern does not include automatic retry around the final write.
Use bulk_create_with_identifiers() when package-managed collision recovery is
desired.
Migration from a Project-Local Identifier Utility
A project-local system may previously contain:
core/identifiers/
├── generator.py
├── mixins.py
└── registry.py
The recommended migration is:
- install
django-identifiers; - replace local mixin imports;
- move model-specific generation policies onto the model;
- replace global registry entries with
AUTO_IDENTIFIERS; - replace old single-generation helpers with the package API;
- replace bulk helper imports;
- keep
unique=Trueon generated identifier fields; - run application tests and migrations only if the application's model field definitions themselves changed;
- delete the duplicated local identifier implementation after migration.
Example old registry policy:
REGISTERED_CODE_STYLES = {
"orders.Order": {
"pattern": "LNLNLNNNN",
},
}
becomes:
class Order(AutoIdentifiersMixin, models.Model):
AUTO_IDENTIFIERS = {
"code": {
"pattern": "LNLNLNNNN",
"immutable": True,
},
}
This keeps application policy with the application model while the reusable generation engine lives in the package.
Supported Versions
| Python | Django 5.2 | Django 6.0 |
|---|---|---|
| 3.11 | Yes | No |
| 3.12 | Yes | Yes |
| 3.13 | Yes | Yes |
| 3.14 | Yes | Yes |
Package metadata currently allows:
Python >= 3.11
Django >= 5.2, < 6.1
Quality
django-identifiers is developed with the same quality standards used across
FIFOA Labs packages.
- Ruff formatting and linting
- Strict mypy validation across source and tests
django-stubs- Pytest and pytest-django
- 100% statement coverage
- 100% branch coverage
- CI across supported Python and Django combinations
- Django system checks
- Source and wheel distribution validation
- Required
py.typedwheel-content validation - Clean-wheel installation testing
- Root-package import smoke testing
- Typed distribution via
py.typed - PyPI Trusted Publishing
Project Status
django-identifiers is suitable for integration and real-world use, but its
public API remains pre-1.0 and may continue to evolve as the package is adopted
by additional Django projects.
The initial release establishes:
- pattern-based identifier generation
- safe character alphabets
- numeric identifier generation
- model-aware best-effort collision checks
- automatic model lifecycle generation
- optional identifier immutability
- explicit immutability escape hatches
- single-object generate-and-save retries
- batch generation
- in-memory bulk assignment
- resilient bulk creation
- concurrent-writer collision recovery
- per-field model configuration
- typed public APIs
Semantic versioning is used:
- patch releases fix bugs and documentation and may refine existing behavior;
- minor
0.xreleases may add features or refine pre-1.0 APIs; 1.0.0will mark a stable public compatibility commitment.
See the PyPI badge and CHANGELOG.md for the currently released version.
Contributing
Issues and pull requests are welcome.
Before submitting changes, run:
make check
make coverage
make build
make check-dist
make install-wheel
For the full local release validation pipeline:
make release-check
See CONTRIBUTING.md for project guidelines.
License
django-identifiers is released under the MIT License.
See LICENSE for the full license text.
Built and maintained by FIFOA Labs.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_identifiers-0.1.0.tar.gz.
File metadata
- Download URL: django_identifiers-0.1.0.tar.gz
- Upload date:
- Size: 29.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b3441c2986f220cfddaef18464aac99782ebc5eab2591e3ae74cc440398ffe66
|
|
| MD5 |
4f52ec671f75eda571ca9c702c520013
|
|
| BLAKE2b-256 |
53469eb50a8ff241b5b3a97a40ab49aac541653bff04788d4db6e9b13cb4357c
|
Provenance
The following attestation bundles were made for django_identifiers-0.1.0.tar.gz:
Publisher:
publish.yml on fifoa-labs/django-identifiers
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_identifiers-0.1.0.tar.gz -
Subject digest:
b3441c2986f220cfddaef18464aac99782ebc5eab2591e3ae74cc440398ffe66 - Sigstore transparency entry: 2582716117
- Sigstore integration time:
-
Permalink:
fifoa-labs/django-identifiers@81c420e33612eccc5b5ca571bcdc6ef2cf0c2415 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/fifoa-labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@81c420e33612eccc5b5ca571bcdc6ef2cf0c2415 -
Trigger Event:
release
-
Statement type:
File details
Details for the file django_identifiers-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_identifiers-0.1.0-py3-none-any.whl
- Upload date:
- Size: 21.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
07dd1d68ab5dded5934d51bd7e5273277fd915f56084f408e0150a4ffbedd577
|
|
| MD5 |
f08d91453802a5606c95ec9403419b65
|
|
| BLAKE2b-256 |
c95429b97f00c00afd14abf4e31765b7ef4cb2e0662ab3e599f28d7bfa670842
|
Provenance
The following attestation bundles were made for django_identifiers-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on fifoa-labs/django-identifiers
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_identifiers-0.1.0-py3-none-any.whl -
Subject digest:
07dd1d68ab5dded5934d51bd7e5273277fd915f56084f408e0150a4ffbedd577 - Sigstore transparency entry: 2582716119
- Sigstore integration time:
-
Permalink:
fifoa-labs/django-identifiers@81c420e33612eccc5b5ca571bcdc6ef2cf0c2415 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/fifoa-labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@81c420e33612eccc5b5ca571bcdc6ef2cf0c2415 -
Trigger Event:
release
-
Statement type: