diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 81b3043eb7e75bc..e94e1dbf049b168 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -1489,6 +1489,24 @@ def test_putenv_unsetenv(self): stdout=subprocess.PIPE, text=True) self.assertEqual(proc.stdout.rstrip(), repr(None)) + @unittest.skipUnless(sys.platform == "win32", "Windows specific test") + @support.requires_subprocess() + def test_environ_empty_value(self): + name = "PYTHONTESTVAR" + code = f'import os; print(repr(os.environ.get({name!r})))' + + with os_helper.EnvironmentVarGuard() as env: + env.pop(name, None) + os.environ[name] = '' + self.assertEqual(os.environ[name], '') + + proc = subprocess.run([sys.executable, '-c', code], check=True, + stdout=subprocess.PIPE, text=True) + self.assertEqual(proc.stdout.rstrip(), repr('')) + + os.reload_environ() + self.assertEqual(os.environ[name], '') + # On OS X < 10.6, unsetenv() doesn't return a value (bpo-13415). @support.requires_mac_ver(10, 6) def test_putenv_unsetenv_error(self): diff --git a/Misc/NEWS.d/next/Windows/2026-10-08-22-30-00.gh-issue-158694.slaiagent.rst b/Misc/NEWS.d/next/Windows/2026-10-08-22-30-00.gh-issue-158694.slaiagent.rst new file mode 100644 index 000000000000000..1796c7e58918a71 --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2026-10-08-22-30-00.gh-issue-158694.slaiagent.rst @@ -0,0 +1 @@ +On Windows, :data:`os.environ` now preserves empty environment variable values in child processes and across :func:`os.reload_environ`. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 028270e7b841fda..9fd4885b15af0f5 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -1792,21 +1792,59 @@ static PyObject * convertenviron(void) { PyObject *d; -#ifdef MS_WINDOWS - wchar_t **e; -#else - char **e; -#endif d = PyDict_New(); if (d == NULL) return NULL; #ifdef MS_WINDOWS - /* _wenviron must be initialized in this way if the program is started - through main() instead of wmain(). */ - (void)_wgetenv(L""); - e = _wenviron; -#elif defined(USE_DARWIN_NS_GET_ENVIRON) + wchar_t *environment = GetEnvironmentStringsW(); + if (environment == NULL) { + Py_DECREF(d); + return PyErr_SetFromWindowsErr(0); + } + + for (wchar_t *entry = environment; *entry != L'\0'; + entry += wcslen(entry) + 1) + { + /* Entries whose names start with '=' are hidden drive-current- + directory variables. The CRT does not expose them, so continue + to omit them from os.environ. */ + if (*entry == L'=') { + continue; + } + + const wchar_t *p = wcschr(entry, L'='); + if (p == NULL) { + continue; + } + + PyObject *k = PyUnicode_FromWideChar(entry, (Py_ssize_t)(p - entry)); + if (k == NULL) { + FreeEnvironmentStringsW(environment); + Py_DECREF(d); + return NULL; + } + PyObject *v = PyUnicode_FromWideChar(p + 1, -1); + if (v == NULL) { + Py_DECREF(k); + FreeEnvironmentStringsW(environment); + Py_DECREF(d); + return NULL; + } + if (PyDict_SetDefaultRef(d, k, v, NULL) < 0) { + Py_DECREF(v); + Py_DECREF(k); + FreeEnvironmentStringsW(environment); + Py_DECREF(d); + return NULL; + } + Py_DECREF(k); + Py_DECREF(v); + } + FreeEnvironmentStringsW(environment); +#else + char **e; +#ifdef USE_DARWIN_NS_GET_ENVIRON /* environ is not accessible as an extern in a shared object on OSX; use _NSGetEnviron to resolve it. The value changes if you add environment variables between calls to Py_Initialize, so don't cache the value. */ @@ -1817,29 +1855,16 @@ convertenviron(void) if (e == NULL) return d; for (; *e != NULL; e++) { - PyObject *k; - PyObject *v; -#ifdef MS_WINDOWS - const wchar_t *p = wcschr(*e, L'='); -#else const char *p = strchr(*e, '='); -#endif if (p == NULL) continue; -#ifdef MS_WINDOWS - k = PyUnicode_FromWideChar(*e, (Py_ssize_t)(p-*e)); -#else - k = PyBytes_FromStringAndSize(*e, (Py_ssize_t)(p-*e)); -#endif + + PyObject *k = PyBytes_FromStringAndSize(*e, (Py_ssize_t)(p - *e)); if (k == NULL) { Py_DECREF(d); return NULL; } -#ifdef MS_WINDOWS - v = PyUnicode_FromWideChar(p+1, -1); -#else - v = PyBytes_FromStringAndSize(p+1, strlen(p+1)); -#endif + PyObject *v = PyBytes_FromStringAndSize(p + 1, strlen(p + 1)); if (v == NULL) { Py_DECREF(k); Py_DECREF(d); @@ -1854,6 +1879,7 @@ convertenviron(void) Py_DECREF(k); Py_DECREF(v); } +#endif return d; } @@ -13777,6 +13803,25 @@ win32_putenv(PyObject *name, PyObject *value) PyMem_Free(env); return NULL; } + + if (value != NULL && PyUnicode_GET_LENGTH(value) == 0) { + /* The CRT treats "NAME=" as removing NAME. Preserve the empty value + in the PEB so that os.environ, os.reload_environ(), and child + processes observe the value requested by the Python caller. */ + wchar_t *name_wstr = PyUnicode_AsWideCharString(name, NULL); + if (name_wstr == NULL) { + PyMem_Free(env); + return NULL; + } + if (!SetEnvironmentVariableW(name_wstr, L"")) { + DWORD error = GetLastError(); + PyMem_Free(name_wstr); + PyMem_Free(env); + return PyErr_SetFromWindowsErr(error); + } + PyMem_Free(name_wstr); + } + PyMem_Free(env); Py_RETURN_NONE;