Skip to main content

Django DataclassConf

CI PyPI - Version License: MIT

A simple Django package setting loader that utilizes dataclasses for type hinting and type checking.

Bring modern Python typing, robust validation, and full IDE autocomplete to your Django configurations.

django-dataclassconf allows you to bind your Django settings cleanly to structured standard Python dataclasses. Powered by dacite, it automatically catches dynamic setting updates while ensuring your configuration layer stays type-safe and isolated.

But Why?

  • Fail-Fast Validation: Catch bad configuration types or missing values instantly during server startup or container deployment rather than hitting silent runtime crashes mid-request.

  • Full IDE Autocomplete: Say goodbye to blind getattr(settings, "MY_SETTING") calls. Enjoy full hovering type definitions and autocompletion in VS Code, PyCharm, and MyPy.

  • Dual Format Normalization: Merges flat environment styles (PREFIX_TIMEOUT = 30) and structured dictionary blocks (PREFIX = {"TIMEOUT": 30}) into a single unified object seamlessly.

  • Test-Safe Isolation: Fully supports Django's test suite cycles. When settings are overridden dynamically in unit tests, your dataclasses mutate cleanly in-place and revert automatically.

Usage

1. Define Your Configuration Dataclass

Create a file named config.py (or any name you prefer tbh) within your application or package. Inherit from BaseConfig and define your variables.

from dataclasses import dataclass
from django_dataclassconf.conf import BaseConfig, config_loader

@dataclass
class MyPackageConfig(BaseConfig):
    DOCUMENTS_ROOT_PATH: str = '/the/default/path/'
    MAX_FILE_SIZE: int = 10000

    @property
    def _prefix(self) -> str:
        """
        Define the config's prefix here. Return a blank string if it doesn't
        have a prefix, such as when writing a configuration dataclass that
        will hold the `DEBUG` setting.
        """
        return 'MY_PACKAGE'

package_config = MyPackageConfig()

2. Subscribe to the Configuration Loader

For your dataclass to grab configuration data from Django's settings.py on startup and capture updates during tests, subscribe your instance into config_loader inside your app's initialization hook:

# my_app/apps.py
class MyAppConfig(AppConfig):
    name = 'my_app'

    def ready(self):
        from django_dataclassconf.conf import config_loader
        from .config import package_config

        config_loader.subscribe(package_config)

3. Access Your Settings Anywhere

The core practice is to import your configuration instance directly instead of using the global django.conf.settings object, to get full type safety and IDE autocomplete.

# my_app/views.py
from django.http import HttpResponse
from my_app.config import package_config

def my_view(request):
    ...
    # Your IDE now natively autocompletes these fields
    if file_size > package_config.MAX_FILE_SIZE:
        return HttpResponse(
            {'detail': 'File size has exceeded the maximum size limit!'}, 
            status = 400
        )

Extras

Nested Dataclasses

Your configuration dataclass can also contain nested dataclasses. You only need to inherit BaseConfig on the root configuration dataclass.

from dataclasses import dataclass, field
from django_dataclassconf.conf import BaseConfig, config_loader

@dataclass
class DocumentPreview:
    preview_page_count: int = 10
    strip_cover_page: bool = False

@dataclass
class MyPackageConfig(BaseConfig):
    DOCUMENTS_ROOT_PATH: str = '/the/default/path/'
    PREVIEW: DocumentPreview = field(default_factory=DocumentPreview)

    @property
    def _prefix(self) -> str:
        return 'MY_PACKAGE'

configuration = MyPackageConfig()
config_loader.subscribe(configuration)

Which maps to this in settings.py:

MY_PACKAGE = {
    'DOCUMENTS_ROOT_PATH': '/custom/path/',
    'PREVIEW': {
        'preview_page_count': 12,
        'strip_cover_page': True
    },
}

Built-in Field Types

Importable

For settings that hold a dotted import path to a class, instance, or callable in another module, annotate them with Importable from fields.py. The import is deferred — nothing is resolved until you explicitly call .resolve().

from dataclasses import dataclass
from django_dataclassconf.fields import Importable
import typing

from .models import Segment

@dataclass
class MyConfig(BaseConfig):
    SEGMENTER_FUNC: Importable[typing.Callable] = 'myapp.utils.segment_audio'
    SEGMENT_SERIALIZER_CLASS: Importable[typing.Type[Serializer]] = 'myapp.serializers.SegmentSerializer'
    SEGMENT_MODEL: Importable[typing.Type[Segment]] = 'myapp.Segment'

configuration = MyConfig()

Call .resolve() when you need the actual object. It validates the type on first call and caches the result:

# Raises ImportError if the path is invalid, or TypeError if the
# resolved object does not match the annotated type.
try:
    segment_model = configuration.SEGMENT_MODEL.resolve()

except ImportError as ie:
    print(f'Could not import SEGMENT_MODEL: {ie}')

except TypeError as te:
    print(f'Imported value has different type than expected: {te}')

Path

For settings that hold a filesystem path, annotate them with Path from fields.py. The raw value may be a str or any os.PathLike; calling .resolve() validates it and returns a pathlib.Path.

from dataclasses import dataclass
from django_dataclassconf.fields import Path

@dataclass
class MyConfig(BaseConfig):
    DOCUMENTS_ROOT: Path[str] = '/the/default/path/'

    @property
    def _prefix(self):
        return 'MY_PACKAGE'

configuration = MyConfig()

Call .resolve() to get the pathlib.Path:

# Returns pathlib.Path('/the/default/path/')
root = configuration.DOCUMENTS_ROOT.resolve()

document = root / 'report.pdf'

Note: Path intentionally shares its name with pathlib.Path. Import it under an alias (e.g. from django_dataclassconf.fields import Path as PathField) if you also need pathlib.Path in the same module.

Custom Field Types

Introduced in version 0.3.0.

You can define your own field types with custom validation and transformation logic by subclassing three base classes from fields.py: FieldValue, FieldGeneric, and Field.

Each custom field type requires three pieces:

  • FieldValue subclass — holds the raw value, implements validate() (raises on invalid input) and resolve() (returns the final value).
  • FieldGeneric subclass — the runtime object produced by YourField[T]. Implements instanciate() to construct the FieldValue.
  • Field subclass — the annotation class used in dataclass definitions. Points at the FieldGeneric via _generic_class.

Simple Example: Email Field

A straightforward validator that checks the value is a valid email address before returning it as a plain string.

from django_dataclassconf.fields import Field, FieldGeneric, FieldValue
import typing


class EmailValue(FieldValue[str]):
    def __init__(self, value: str):
        self.value = value

    def validate(self):
        if not isinstance(self.value, str) or '@' not in self.value:
            raise ValueError(f'{self.value!r} is not a valid email address')

    def resolve(self) -> str:
        self.validate()
        return self.value

    def __repr__(self):
        return f'EmailValue({self.value!r})'

    def __eq__(self, other):
        if isinstance(other, EmailValue):
            return self.value == other.value
        return NotImplemented

    def __hash__(self):
        return hash(self.value)


class _EmailGeneric(FieldGeneric['EmailValue']):
    def __repr__(self):
        return f'Email[{self.__inner_type__}]'

    def instanciate(self, value: str) -> EmailValue:
        return EmailValue(value)


if typing.TYPE_CHECKING:
    Email = EmailValue

else:
    class Email(Field):
        _generic_class = _EmailGeneric

Use it in your configuration dataclass the same way as any built-in field type:

@dataclass
class MyConfig(BaseConfig):
    ADMIN_EMAIL: Email[str] = 'admin@example.com'

    @property
    def _prefix(self):
        return 'MY_APP'

Call .resolve() to get the validated value, or check .is_valid if you want a boolean without raising:

# Raises ValueError if the configured value is not a valid email address
admin_email = my_config.ADMIN_EMAIL.resolve()

# Non-raising check
if my_config.ADMIN_EMAIL.is_valid:
    ...

Advanced Example: Deprecated Field

A field that emits a DeprecationWarning when validated, useful for marking settings that are still supported but scheduled for removal.

from django_dataclassconf.fields import Field, FieldGeneric, FieldValue
import warnings
import typing


class DeprecatedValue(FieldValue):
    def __init__(self, value: typing.Any, inner_type: typing.Type[typing.Any]):
        self.value = value
        self.inner_type = inner_type

    def validate(self):
        if not isinstance(self.value, self.inner_type):
            raise TypeError(
                f'Instance {self.value} is not of type {self.inner_type.__name__}'
            )
        warnings.warn('This setting is deprecated', DeprecationWarning, stacklevel=2)

    def resolve(self):
        return self.value

    def __repr__(self):
        return repr(self.value)

    def __eq__(self, other):
        if isinstance(other, DeprecatedValue):
            return self.value == other.value and self.inner_type == other.inner_type
        return NotImplemented

    def __hash__(self):
        return hash((repr(self.value), repr(self.inner_type)))


class _DeprecatedGeneric(FieldGeneric['DeprecatedValue']):
    def __repr__(self):
        return f'Deprecated[{self.__inner_type__}]'

    def instanciate(self, value) -> DeprecatedValue:
        return DeprecatedValue(value, self.__inner_type__)


if typing.TYPE_CHECKING:
    Deprecated = DeprecatedValue

else:
    class Deprecated(Field):
        _generic_class = _DeprecatedGeneric
@dataclass
class MyConfig(BaseConfig):
    LEGACY_PATH: Deprecated[str] = '/old/default/path/'

    @property
    def _prefix(self):
        return 'MY_APP'

Calling .validate() on a Deprecated field emits the warning without raising, as long as the value is the correct type:

import warnings

with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter('always')
    my_config.LEGACY_PATH.validate()

# caught[0].category is DeprecationWarning

License

This project is licensed under the MIT License — see the LICENSE file for details.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_dataclassconf-0.4.0.tar.gz (16.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_dataclassconf-0.4.0-py3-none-any.whl (14.3 kB view details)

Uploaded Python 3

File details

Details for the file django_dataclassconf-0.4.0.tar.gz.

File metadata

  • Download URL: django_dataclassconf-0.4.0.tar.gz
  • Upload date:
  • Size: 16.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for django_dataclassconf-0.4.0.tar.gz
Algorithm Hash digest
SHA256 a0e02feead47f861edd1daacc15eeba4aeb6e7f298c08a3fbd747ca83042c809
MD5 fafdbcda3a2b7476c5c498017ce7b68b
BLAKE2b-256 b2447e83391eb14bc11318e853a97862f8fbd217f06e2f7e4dd5af3ccd769d73

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_dataclassconf-0.4.0.tar.gz:

Publisher: pipeline.yml on Jxst-Felix/django-dataclassconf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_dataclassconf-0.4.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_dataclassconf-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8db9205fac6e43e6f4882a6aec4732225f0751f4e9364be32b55b2f66a241b40
MD5 bb274981cb6c6f0d16c39d0a720d06a5
BLAKE2b-256 8125bc3993aeeef5ecaf1c7fb3e282ac26399c9fa168700d78c729ddf8ec1579

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_dataclassconf-0.4.0-py3-none-any.whl:

Publisher: pipeline.yml on Jxst-Felix/django-dataclassconf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page