From d59a10203be0e7f58f443207c4d91265e6d6853a Mon Sep 17 00:00:00 2001 From: Sakethram reddy Date: Mon, 14 Sep 2026 22:56:30 -0400 Subject: [PATCH] Add unload_dotenv to reverse load_dotenv in tests. Keys are removed from os.environ only when the current value still matches the .env file, so later assignments are left alone. Closes #594 Co-authored-by: Cursor --- README.md | 4 ++++ src/dotenv/__init__.py | 3 ++- src/dotenv/main.py | 29 +++++++++++++++++++++++++++++ tests/test_main.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a08d6141..8df34433 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,10 @@ load_dotenv() # reads variables from a .env file and sets them in os.environ # Code of your application, which uses environment variables (e.g. from `os.environ` or # `os.getenv`) as if they came from the actual environment. + +# In tests, unload_dotenv() removes those values from os.environ again: +# from dotenv import unload_dotenv +# unload_dotenv() ``` By default, `load_dotenv()` will: diff --git a/src/dotenv/__init__.py b/src/dotenv/__init__.py index dde24a01..e7e5a300 100644 --- a/src/dotenv/__init__.py +++ b/src/dotenv/__init__.py @@ -1,6 +1,6 @@ from typing import Any, Optional -from .main import dotenv_values, find_dotenv, get_key, load_dotenv, set_key, unset_key +from .main import dotenv_values, find_dotenv, get_key, load_dotenv, set_key, unload_dotenv, unset_key def load_ipython_extension(ipython: Any) -> None: @@ -42,6 +42,7 @@ def get_cli_string( __all__ = [ "get_cli_string", "load_dotenv", + "unload_dotenv", "dotenv_values", "get_key", "set_key", diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 3123690a..111a8b2f 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -435,6 +435,35 @@ def load_dotenv( return dotenv.set_as_environment_variables() +def unload_dotenv( + dotenv_path: Optional[StrPath] = None, + stream: Optional[IO[str]] = None, + encoding: Optional[str] = "utf-8", +) -> bool: + """Remove variables defined in a `.env` file from `os.environ`. + + Useful in tests so `load_dotenv()` does not leak into later cases. + + A key is removed only when its current environment value still matches the + value from the file, so an explicit later assignment is left alone. + + Returns True if at least one variable was removed. + """ + values = dotenv_values( + dotenv_path=dotenv_path, + stream=stream, + encoding=encoding, + ) + removed = False + for key, value in values.items(): + if value is None: + continue + if os.environ.get(key) == value: + del os.environ[key] + removed = True + return removed + + def dotenv_values( dotenv_path: Optional[StrPath] = None, stream: Optional[IO[str]] = None, diff --git a/tests/test_main.py b/tests/test_main.py index 6f9d4c5c..77d94448 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -425,6 +425,36 @@ def test_load_dotenv_existing_file(dotenv_path): assert os.environ == {"a": "b"} +@mock.patch.dict(os.environ, {}, clear=True) +def test_unload_dotenv_removes_loaded_values(dotenv_path): + dotenv_path.write_text("a=b") + dotenv.load_dotenv(dotenv_path) + + result = dotenv.unload_dotenv(dotenv_path) + + assert result is True + assert "a" not in os.environ + + +@mock.patch.dict(os.environ, {}, clear=True) +def test_unload_dotenv_keeps_changed_values(dotenv_path): + dotenv_path.write_text("a=b") + dotenv.load_dotenv(dotenv_path) + os.environ["a"] = "later" + + result = dotenv.unload_dotenv(dotenv_path) + + assert result is False + assert os.environ["a"] == "later" + + +@mock.patch.dict(os.environ, {}, clear=True) +def test_unload_dotenv_missing_file(tmp_path): + result = dotenv.unload_dotenv(tmp_path / "missing.env") + + assert result is False + + @pytest.mark.parametrize( "flag_value", [