Is your feature request related to a problem?
Currently, __repr__ of yarl.URL (from yarl) includes the raw password when a URL contains user credentials:
>>> from yarl import URL
>>> URL("https://user:secret@example.com")
URL('https://user:secret@example.com')
This behavior can unintentionally expose sensitive credentials in logs, tracebacks, debugging sessions, or error reporting systems where repr() is implicitly used.
Describe the solution you'd like
__repr__ should mask the password component, similar to how many HTTP clients and URL tooling libraries handle credential rendering. For example:
>>> from yarl import URL
>>> URL('https://user:secret@example.com')
URL('https://user:********@example.com')
or another clearly redacted placeholder.
Rationale
repr() is frequently surfaced in logs and diagnostics.
- Accidental credential leakage via logging is a common operational risk.
- Masking credentials in representations aligns with the principle of least exposure.
- This change makes the ecosystem incrementally ("epsilon") safer with minimal behavioral disruption.
Importantly, this proposal affects only the string representation — not the internal storage or functional behavior of the URL object.
Backward Compatibility Considerations
- The change alters the exact
repr() output, which may affect strict string-based tests.
- However, this impact is limited and security-positive.
- If necessary, the full URL (including password) remains accessible via explicit API accessors or by
str().
Describe alternatives you've considered
Creating a subclass from the yarl.URL to implement masking logic. However, inheriting from the URL class is forbidden:
>>> class MyUrl(yarl.URL):
... def __repr__(self):
... return "SECRET"
...
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "********/.venv/Lib/site-packages/yarl/_url.py", line 482, in __init_subclass__
raise TypeError(f"Inheriting a class {cls!r} from URL is forbidden")
TypeError: Inheriting a class <class '__main__.MyUrl'> from URL is forbidden
Additional context
We can make a PR request for this specific change. Below is a patch of the required changes:
Index: CHANGES/1629.feature.rst
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/CHANGES/1629.feature.rst b/CHANGES/1629.feature.rst
new file mode 100644
--- /dev/null (revision 2f914e6f3a72719e30dd434c99609159aa562c57)
+++ b/CHANGES/1629.feature.rst (revision 2f914e6f3a72719e30dd434c99609159aa562c57)
@@ -0,0 +1,2 @@
+Mask the password component in ``yarl.URL.__repr__`` to prevent accidental credential exposure in logs and debug
+output -- by :user:`mbaas038` and :user:`jhbuhrman`.
Index: tests/test_url.py
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/tests/test_url.py b/tests/test_url.py
--- a/tests/test_url.py (revision 7829b01ec6217349dfce3a1dd3190d29ddbb14a9)
+++ b/tests/test_url.py (revision 2f914e6f3a72719e30dd434c99609159aa562c57)
@@ -57,13 +57,13 @@
def test_str() -> None:
- url = URL("http://example.com:8888/path/to?a=1&b=2")
- assert str(url) == "http://example.com:8888/path/to?a=1&b=2"
+ url = URL("http://user:password@example.com:8888/path/to?a=1&b=2")
+ assert str(url) == "http://user:password@example.com:8888/path/to?a=1&b=2"
def test_repr() -> None:
- url = URL("http://example.com")
- assert "URL('http://example.com')" == repr(url)
+ url = URL("http://user:password@example.com")
+ assert "URL('http://user:********@example.com')" == repr(url)
def test_origin() -> None:
Index: yarl/_parse.py
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/yarl/_parse.py b/yarl/_parse.py
--- a/yarl/_parse.py (revision 7829b01ec6217349dfce3a1dd3190d29ddbb14a9)
+++ b/yarl/_parse.py (revision 2f914e6f3a72719e30dd434c99609159aa562c57)
@@ -161,6 +161,7 @@
host: str | None,
port: int | None,
encode: bool = False,
+ mask_password: bool = False,
) -> str:
"""Make netloc from parts.
@@ -176,6 +177,8 @@
if user is None and password is None:
return ret
if password is not None:
+ if mask_password:
+ password = "********"
if not user:
user = ""
elif encode:
Index: yarl/_url.py
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/yarl/_url.py b/yarl/_url.py
--- a/yarl/_url.py (revision 7829b01ec6217349dfce3a1dd3190d29ddbb14a9)
+++ b/yarl/_url.py (revision 2f914e6f3a72719e30dd434c99609159aa562c57)
@@ -491,24 +491,31 @@
def __init_subclass__(cls) -> NoReturn:
raise TypeError(f"Inheriting a class {cls!r} from URL is forbidden")
- def __str__(self) -> str:
+ def _make_string(self, mask_password: bool = False) -> str:
if not self._path and self._netloc and (self._query or self._fragment):
path = "/"
else:
path = self._path
- if (port := self.explicit_port) is not None and port == DEFAULT_PORTS.get(
- self._scheme
+ if (
+ (port := self.explicit_port) is not None
+ and port == DEFAULT_PORTS.get(self._scheme)
+ or mask_password
):
# port normalization - using None for default ports to remove from rendering
# https://datatracker.ietf.org/doc/html/rfc3986.html#section-6.2.3
host = self.host_subcomponent
- netloc = make_netloc(self.raw_user, self.raw_password, host, None)
+ netloc = make_netloc(
+ self.raw_user, self.raw_password, host, None, mask_password=True
+ )
else:
netloc = self._netloc
return unsplit_result(self._scheme, netloc, path, self._query, self._fragment)
+ def __str__(self) -> str:
+ return self._make_string(mask_password=False)
+
def __repr__(self) -> str:
- return f"{self.__class__.__name__}('{str(self)}')"
+ return f"{self.__class__.__name__}('{self._make_string(mask_password=True)}')"
def __bytes__(self) -> bytes:
return str(self).encode("ascii")
This patch was kindly supported by Sopra Steria Pythoneers
Code of Conduct
Is your feature request related to a problem?
Currently,
__repr__ofyarl.URL(from yarl) includes the raw password when a URL contains user credentials:This behavior can unintentionally expose sensitive credentials in logs, tracebacks, debugging sessions, or error reporting systems where
repr()is implicitly used.Describe the solution you'd like
__repr__should mask the password component, similar to how many HTTP clients and URL tooling libraries handle credential rendering. For example:or another clearly redacted placeholder.
Rationale
repr()is frequently surfaced in logs and diagnostics.Importantly, this proposal affects only the string representation — not the internal storage or functional behavior of the URL object.
Backward Compatibility Considerations
repr()output, which may affect strict string-based tests.str().Describe alternatives you've considered
Creating a subclass from the
yarl.URLto implement masking logic. However, inheriting from theURLclass is forbidden:Additional context
We can make a PR request for this specific change. Below is a patch of the required changes:
This patch was kindly supported by Sopra Steria Pythoneers
Code of Conduct