From 3b1bbb9043fabed9ae06c78ca39ef7ec2ff21fa0 Mon Sep 17 00:00:00 2001 From: Boris Kozak Date: Sun, 4 Oct 2026 15:13:14 -0400 Subject: [PATCH 1/2] Fix ${name:-default} interpolation ignoring default for empty values Per POSIX, ${name:-default} should expand to the default when name is unset or empty. Previously an explicitly empty value (e.g. `b=`) was returned as-is instead of falling back to the default. Fixes #715 --- src/dotenv/variables.py | 8 +++++++- tests/test_main.py | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/dotenv/variables.py b/src/dotenv/variables.py index 667f2f26..1c32e1da 100644 --- a/src/dotenv/variables.py +++ b/src/dotenv/variables.py @@ -64,7 +64,13 @@ def __hash__(self) -> int: def resolve(self, env: Mapping[str, Optional[str]]) -> str: default = self.default if self.default is not None else "" result = env.get(self.name, default) - return result if result is not None else "" + if result is None: + return "" + if result == "" and self.default is not None: + # POSIX: ${name:-default} falls back to the default when name + # is unset or empty (issue #715) + return default + return result def parse_variables(value: str) -> Iterator[Atom]: diff --git a/tests/test_main.py b/tests/test_main.py index 930ab171..c2ada860 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -894,6 +894,10 @@ def test_dotenv_values_file(dotenv_path): # Undefined ({}, "a=${b}", True, {"a": ""}), ({}, "a=${b:-d}", True, {"a": "d"}), + # Empty value with default: ${name:-default} treats empty as unset (#715) + ({}, "b=\na=${b:-d}", True, {"a": "d", "b": ""}), + ({"b": ""}, "a=${b:-d}", True, {"a": "d"}), + ({}, "b=\na=${b}", True, {"a": "", "b": ""}), # With quotes ({"b": "c"}, 'a="${b}"', True, {"a": "c"}), ({"b": "c"}, "a='${b}'", True, {"a": "c"}), From c0cd3e211773f97422e5ce9522e91def51bf925a Mon Sep 17 00:00:00 2001 From: Boris Kozak Date: Wed, 14 Oct 2026 23:30:06 +0000 Subject: [PATCH 2/2] fix: use ${name:-default} fallback for bare declarations A bare declaration like `BASE` parses to None. Under ${name:-default} that is the null case, same as unset, so resolve to the default instead of "". Adds regression cases next to the #715 empty-value tests. --- src/dotenv/variables.py | 5 ++++- tests/test_main.py | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/dotenv/variables.py b/src/dotenv/variables.py index 1c32e1da..719e4108 100644 --- a/src/dotenv/variables.py +++ b/src/dotenv/variables.py @@ -65,7 +65,10 @@ def resolve(self, env: Mapping[str, Optional[str]]) -> str: default = self.default if self.default is not None else "" result = env.get(self.name, default) if result is None: - return "" + # A bare declaration (e.g. `BASE` with no value) parses to None. + # POSIX: under ${name:-default} that is the null case, same as + # unset, so fall back to the default when one was given. + return default if result == "" and self.default is not None: # POSIX: ${name:-default} falls back to the default when name # is unset or empty (issue #715) diff --git a/tests/test_main.py b/tests/test_main.py index c2ada860..56f6f514 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -898,6 +898,10 @@ def test_dotenv_values_file(dotenv_path): ({}, "b=\na=${b:-d}", True, {"a": "d", "b": ""}), ({"b": ""}, "a=${b:-d}", True, {"a": "d"}), ({}, "b=\na=${b}", True, {"a": "", "b": ""}), + # Bare declaration (no value, parses to None) with default: + # ${name:-default} treats it as unset/null and uses the default + ({}, "b\na=${b:-d}", True, {"a": "d", "b": None}), + ({}, "b\na=${b}", True, {"a": "", "b": None}), # With quotes ({"b": "c"}, 'a="${b}"', True, {"a": "c"}), ({"b": "c"}, "a='${b}'", True, {"a": "c"}),