Skip to content

Add stubs for django-environ#14573

Open
EmCeeEs wants to merge 20 commits intopython:mainfrom
EmCeeEs:feature/django-environ-types
Open

Add stubs for django-environ#14573
EmCeeEs wants to merge 20 commits intopython:mainfrom
EmCeeEs:feature/django-environ-types

Conversation

@EmCeeEs
Copy link
Copy Markdown

@EmCeeEs EmCeeEs commented Aug 13, 2025

Types for django-environ.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@donbarbos
Copy link
Copy Markdown
Contributor

Thanks! I just want to note that the project doesn’t seem to be very actively maintained (the last commit was 7 months ago), so I wouldn’t expect type hints to be added to the library itself anytime soon.

FTR: here’s a recent issue essentially complaining about the lack of type hints: joke2k/django-environ#567, and another one joke2k/django-environ#365 that might give some insight into the maintainer’s stance on adding them.

Copy link
Copy Markdown
Contributor

@donbarbos donbarbos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Just few moments:

Comment thread stubs/django-environ/environ/__init__.pyi Outdated
Comment thread stubs/django-environ/environ/compat.pyi Outdated
Comment thread stubs/django-environ/environ/compat.pyi Outdated
Comment thread stubs/django-environ/environ/environ.pyi Outdated
Comment thread stubs/django-environ/environ/environ.pyi Outdated
Copy link
Copy Markdown
Collaborator

@brianschubert brianschubert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Looks good overall, see remarks below

Comment thread stubs/django-environ/environ/__init__.pyi Outdated
Comment thread stubs/django-environ/environ/compat.pyi
Comment thread stubs/django-environ/environ/compat.pyi Outdated
Comment thread stubs/django-environ/environ/environ.pyi Outdated
Comment thread stubs/django-environ/environ/environ.pyi Outdated
Comment thread stubs/django-environ/environ/environ.pyi Outdated
Comment thread stubs/django-environ/environ/environ.pyi Outdated
Comment thread stubs/django-environ/environ/environ.pyi Outdated
Comment thread stubs/django-environ/environ/environ.pyi Outdated
Comment thread stubs/django-environ/environ/fileaware_mapping.pyi Outdated
@brianschubert brianschubert changed the title Feature/django environ types Add stubs for django-environ Aug 13, 2025
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@donbarbos
Copy link
Copy Markdown
Contributor

I noticed that you’ve added quite a few Any types in generics. Just a gentle reminder that the use of Any is usually accompanied by a short comment explaining why it’s needed. If you’re not entirely sure about the type yet, you might consider using Incomplete instead. It could be a more suitable choice in such cases.

Comment thread stubs/django-environ/METADATA.toml Outdated
Comment on lines 180 to 182
def __call__(
self, var: _str, cast: type[_dict], default: _dict[_str, _str] | NoValue = ..., parse_default: _bool = False
self, var: _str, cast: type[_dict[Any, Any]], default: _dict[_str, _str] | NoValue = ..., parse_default: _bool = False
) -> _dict[_str, _str]: ...
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that you’ve added quite a few Any types in generics. Just a gentle reminder that the use of Any is usually accompanied by a short comment explaining why it’s needed. If you’re not entirely sure about the type yet, you might consider using Incomplete instead. It could be a more suitable choice in such cases.

@donbarbos You are right. However, what I need is type[dict] of the builtin function. I think it's awkward, but pyright requires type arguments in that case. So I added the Any. Not sure if comments or replace with Incomplete makes more sense here.

Copy link
Copy Markdown
Contributor

@donbarbos donbarbos Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you can't specify types just leave Incomplete, this is a good case.
Anyway in the future it could be improved (type Any is not very suitable as a placeholder)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replaced Any with Incomplete or object where applicable. Remaining Anys are explained with comments.

@github-actions

This comment has been minimized.


# cast dict values
assert_type(env.parse_value("0=TRUE,99=FALSE", {}), dict[str, str])
assert_type(env.parse_value("0=TRUE,99=FALSE", {"cast": {}}), dict[str, Union[str, object]])
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the inferred type for this case should ideally be dict[str, str]. Inferring dict[str, object] makes this awkward to use for end users, since you can't use the values in the dictionary without casting them or using a type narrowing construct

Comment on lines +59 to +61
def cast_person(v: str) -> Person:
parts = v.split(",")
return {"first_name": parts[0], "last_name": parts[1], "age": int(parts[2])}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to include a runtime implementation since these tests aren't executed (we only check how they type check). I think this case is also redundant with custom cast function case above

@@ -0,0 +1,13 @@
from typing import Final

from .compat import DJANGO_POSTGRES as DJANGO_POSTGRES, PYMEMCACHE_DRIVER as PYMEMCACHE_DRIVER, REDIS_DRIVER as REDIS_DRIVER
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are indirectly re-exported from .environ, so let's move the import there

Suggested change
from .compat import DJANGO_POSTGRES as DJANGO_POSTGRES, PYMEMCACHE_DRIVER as PYMEMCACHE_DRIVER, REDIS_DRIVER as REDIS_DRIVER

from typing_extensions import NotRequired, Required, TypeAlias, Unpack
from urllib.parse import ParseResult

from .fileaware_mapping import FileAwareMapping
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above

Suggested change
from .fileaware_mapping import FileAwareMapping
from .compat import DJANGO_POSTGRES as DJANGO_POSTGRES, PYMEMCACHE_DRIVER as PYMEMCACHE_DRIVER, REDIS_DRIVER as REDIS_DRIVER
from .fileaware_mapping import FileAwareMapping

_KT = TypeVar("_KT")
_VT = TypeVar("_VT")

_Cast: TypeAlias = Callable[[str], _T]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'd suggest calling this something like _CastFunc to help distinguish this from the type that's accepted by the various cast parameters, which is different


class Path:
def path(self, *paths: str, **kwargs: Unpack[PathKwargs]) -> Path: ...
def file(self, name: str, *args, **kwargs) -> IO[str]: ...
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could also return a IO[bytes] if e.g. mode="rb" is passed, so let's change this to IO[Any].

Suggested change
def file(self, name: str, *args, **kwargs) -> IO[str]: ...
# *args and **kwargs are passed to open().
def file(self, name: str, *args: Any, **kwargs: Any) -> IO[Any]: ...

Comment on lines +402 to +403
def rfind(self, s: str, sub: str, start: int = 0, end: int = ...) -> int: ...
def find(self, s: str, sub: str, start: int = 0, end: int = ...) -> int: ...
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def rfind(self, s: str, sub: str, start: int = 0, end: int = ...) -> int: ...
def find(self, s: str, sub: str, start: int = 0, end: int = ...) -> int: ...
def rfind(self, s: str, start: SupportsIndex | None = ..., end: SupportsIndex | None = ..., /) -> int: ...
def find(self, s: str, start: SupportsIndex | None = ..., end: SupportsIndex | None = ..., /) -> int: ...


__root__: str

def __init__(self, start: str = "", *paths: str, **kwargs: Unpack[PathKwargs]) -> None: ...
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

start and paths are passed directly to os.path functions, so any pathlike is accepted.

Suggested change
def __init__(self, start: str = "", *paths: str, **kwargs: Unpack[PathKwargs]) -> None: ...
def __init__(self, start: StrPath = "", *paths: StrPath, **kwargs: Unpack[PathKwargs]) -> None: ...

You'll need to import StrPath from _typeshed

__root__: str

def __init__(self, start: str = "", *paths: str, **kwargs: Unpack[PathKwargs]) -> None: ...
def __call__(self, *paths: str, **kwargs: Unpack[PathKwargs]) -> str: ...
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def __call__(self, *paths: str, **kwargs: Unpack[PathKwargs]) -> str: ...
def __call__(self, *paths: StrPath, **kwargs: Unpack[PathKwargs]) -> str: ...

Comment on lines +42 to +46
# One CastDict for each combination of 'key', 'value' and 'cast' (8 in total).
# Use auxiliary '_type' to make them distinguishable for type checkers.
@type_check_only
class CastDict000(TypedDict):
_type: NotRequired[Literal["000"]]
Copy link
Copy Markdown
Collaborator

@brianschubert brianschubert Sep 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite follow what's being done with these TypedDicts. It looks like this is a complicated case that would take some effort to get right. In order to not delay getting the rest of the stubs merged, I'd suggest taking a very simple approach for this PR (say, something inferring dict[Any, Any] for any dict-valued cast argument) and work on refining the dict cast behavior in a followup

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this was quite complicated. I simplified it to just dict[Any, Any] for now.

@brianhelba
Copy link
Copy Markdown

In a lot of my own django-environ usage, it's very useful to have the pattern:

OPTIONAL_SETTING: str | None = environ.Env().str("DJANGO_OPTIONAL_SETTING", default=None)

This is allowed by the library behavior, I'm wondering if the type stubs could also allow something like:

_DefaultT = TypeVar("_DefaultT")

class Env:
    @typing.overload
    def str(self, var: _str, default: _DefaultT = ..., multiline: _bool = False) -> _DefaultT: ...
    @typing.overload
    def str(self, var: _str, multiline: _bool = False) -> _str: 

Regarding this idea:

  1. Is this the correct typing syntax to express this? It feels weird that this omits explicit use of NoValue.
  2. Should the type annotations permit this all? It's behaviorally allowed by the downstream library and is practically useful to me, but is this too abusive of strong typing? Personally, I think it's fine.
  3. Should this be implemented in a different PR? This PR seems stalled, so maybe it's better to make this change separately? It is just widening / liberalizing the type rules for the default parameter, so compatibility should be fine.

@brianhelba
Copy link
Copy Markdown

@EmCeeEs @brianschubert Thanks for all your efforts here!

This PR seems to be inactive now, but I'm very interested in seeing it merged. Is it possible for me to provide any help to finish it?

@joke2k
Copy link
Copy Markdown

joke2k commented Feb 21, 2026

Hi all! I’m actively maintaining this project and I’d be very interested in shipping a 1.0 release with proper typing support. Happy to collaborate to make that happen.

@github-actions

This comment has been minimized.

@github-actions
Copy link
Copy Markdown
Contributor

According to mypy_primer, this change has no effect on the checked open source code. 🤖🎉

@JelleZijlstra
Copy link
Copy Markdown
Member

I pushed changes addressing @brianschubert's feedback; is this good to go in now?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants