ADR 0017: Align load_env() Parameter Signature with python-dotenv's load_dotenv()¶
Status¶
Accepted
Date¶
2025-10-20
Context¶
The original load_env() function signature used path: str | Path = ".env" as its parameter:
This created migration friction for users switching from python-dotenv's load_dotenv(), which uses dotenv_path: str | Path | None = None:
The differences:
- Parameter name:
pathvs.dotenv_path - Default value:
.env(explicit path) vs.None(search behavior) - Search semantics: When
None, python-dotenv searches for.envstarting from the current working directory
Users migrating from python-dotenv had to:
- Rename parameter:
load_dotenv()→load_env(path=...) - Change default behavior:
load_dotenv()→load_env(path=".env")
Decision¶
Change load_env() signature to match python-dotenv's load_dotenv():
def load_env(
dotenv_path: str | Path | None = None,
*,
export: bool = True,
override: bool = False,
) -> dict[str, str]:
"""Load environment variables from a .env file and resolve secret URIs.
Args:
dotenv_path: Path to .env file. If None, searches for .env in
current directory. Mimics python-dotenv's load_dotenv() behavior.
(default: None)
...
"""
# When dotenv_path is None, use find_dotenv with usecwd=True
if dotenv_path is None:
dotenv_path = find_dotenv(usecwd=True)
env_dict = {k: v for k, v in dotenv_values(dotenv_path).items() if v is not None}
...
Implementation Details¶
When dotenv_path=None, we explicitly call find_dotenv(usecwd=True) before passing to dotenv_values(). This is necessary because:
dotenv_values(None)internally callsfind_dotenv()(withoutusecwd=True)find_dotenv()defaults tousecwd=False, which searches from__file__location- Since
api.pyis insrc/envresolve/, the search would start there and walk up to project root - This could find the wrong
.envfile (e.g., project's dev.envinstead of application's.env)
By using find_dotenv(usecwd=True), we ensure the search starts from os.getcwd(), matching user expectations.
Rationale¶
For Users¶
- Zero-friction migration: Drop-in replacement for
load_dotenv() - Intuitive behavior:
Nonemeans "search from current directory" (matches shell behavior) - Familiar API: Same parameter name as python-dotenv
For Maintainability¶
- Explicit control: We control search behavior instead of relying on
dotenv_values()internals - Clear intent:
find_dotenv(usecwd=True)explicitly documents search semantics - Test reliability: Tests that
os.chdir()work correctly because search happens from cwd
Implications¶
Positive Implications¶
- Drop-in replacement for python-dotenv users
- More intuitive default behavior (search from cwd)
- Better aligned with shell conventions (
cdchanges where.envis found)
API Changes¶
The load_env() signature was updated for better python-dotenv compatibility:
Parameter Name¶
Default Value¶
# Before: Explicit path, always loads `.env` in cwd
load_env() # Loads ./.env
# After: None triggers search, finds .env from cwd
load_env() # Searches for .env starting from cwd, walks up if not found
Most users have .env in the current directory, so behavior remains unchanged in practice.
Search Behavior Differences from python-dotenv¶
Important limitation: envresolve.load_env(None) does not have identical search behavior to python-dotenv's load_dotenv(None):
- python-dotenv:
load_dotenv(None)usesfind_dotenv(usecwd=False), which searches from the calling script's file location - envresolve:
load_env(None)usesfind_dotenv(usecwd=True), which searches from the current working directory
Why the difference?
When usecwd=False, find_dotenv() searches from the __file__ location of the calling code. For python-dotenv (a library users import), this means the user's script location. For envresolve, it would mean the library's installed location (e.g., site-packages/envresolve/api.py), which is never the intended behavior.
Using usecwd=True provides the most intuitive behavior: search from where the user is running their script.
For complete python-dotenv compatibility, use this pattern:
from dotenv import load_dotenv
import envresolve
# Use python-dotenv's search behavior
load_dotenv() # Searches from calling script location, loads to os.environ
# Resolve secrets in os.environ
envresolve.register_azure_kv_provider()
envresolve.resolve_os_environ() # Resolves akv:// URIs in os.environ
This approach:
- Preserves python-dotenv's exact search semantics
- Keeps envresolve focused on secret resolution
- Provides clear separation of concerns
Alternatives¶
Keep path parameter, only change default to None¶
Pros: Smaller breaking change (only default value)
Cons:
- Still migration friction (parameter name mismatch)
- Inconsistent with python-dotenv's established API
- Confusing documentation ("path" vs. "dotenv_path")
Rejection reason: Partial migration creates long-term API inconsistency.
Add dotenv_path as alias, deprecate path¶
Pros: Gradual migration path
Cons:
- Increased complexity (two parameters doing the same thing)
- Deprecation warnings add friction
- Eventually needs breaking change anyway
- envresolve is v0.x, users expect breakage
Rejection reason: For v0.x libraries, clean break is better than deprecation cycle.
Keep original signature, add search=False flag¶
Pros: No breaking change
Cons:
- Doesn't solve migration friction (parameter name still wrong)
- More complex API (two parameters to control one behavior)
path=".env", search=Trueis confusing
Rejection reason: Adds complexity without solving the root problem.
Future Direction¶
- Consider adding
load_env_strict(dotenv_path: str | Path)that requires explicit path and never searches - If users request it, add
usecwdparameter to expose python-dotenv's search control:load_env(dotenv_path=None, usecwd=True)
References¶
- Issue: #13 "Align load_env signature with load_dotenv"
- Implementation:
src/envresolve/api.py:129-183 - Tests:
tests/e2e/test_load_env.py - python-dotenv docs: https://saurabh-kumar.com/python-dotenv/
- Related ADRs: 0013 (class-based API design)