From 39aa612c58a4f8b03d859e0aec1a95c920f2d3fc Mon Sep 17 00:00:00 2001 From: jkmnt Date: Thu, 5 Mar 2026 12:59:31 +0300 Subject: [PATCH 1/7] tests first --- tests/brain/test_brain.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/brain/test_brain.py b/tests/brain/test_brain.py index 0b60ac264..29c70a566 100644 --- a/tests/brain/test_brain.py +++ b/tests/brain/test_brain.py @@ -1419,6 +1419,30 @@ def test_infer_str() -> None: assert isinstance(inferred, astroid.Instance) assert inferred.qname() == "builtins.str" +def test_infer_str_const() -> None: + ast_nodes = astroid.extract_node(""" + str('') #@ + str('a') #@ + str(1) #@ + str(True) #@ + str(False) #@ + str(None) #@ + str(4.33) #@ + str(...) #@ + str(2 + 2) #@ + """) + + inferred = list(node.inferred()[0].value for node in ast_nodes) + assert inferred[0] == "" + assert inferred[1] == "a" + assert inferred[2] == "1" + assert inferred[3] == "True" + assert inferred[4] == "False" + assert inferred[5] == "None" + assert inferred[6] == "4.33" + assert inferred[7] == "..." + assert inferred[8] == "4" + def test_infer_int() -> None: ast_nodes = astroid.extract_node(""" From 1228762d88ff8f9c7373f6197437976371744ffd Mon Sep 17 00:00:00 2001 From: jkmnt Date: Thu, 5 Mar 2026 13:31:33 +0300 Subject: [PATCH 2/7] ok --- astroid/brain/brain_builtin_inference.py | 18 +++++++++++++----- tests/brain/test_brain.py | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/astroid/brain/brain_builtin_inference.py b/astroid/brain/brain_builtin_inference.py index a2ca95514..716a84d80 100644 --- a/astroid/brain/brain_builtin_inference.py +++ b/astroid/brain/brain_builtin_inference.py @@ -842,15 +842,23 @@ def infer_str(node, context: InferenceContext | None = None) -> nodes.Const: :param nodes.Call node: str() call to infer :param context.InferenceContext: node context - :rtype nodes.Const: a Const containing an empty string + :rtype nodes.Const: + a Const containing a stringified value of str() call if possible, else an empty string """ call = arguments.CallSite.from_call(node, context=context) if call.keyword_arguments: raise UseInferenceDefault("TypeError: str() must take no keyword arguments") - try: - return nodes.Const("") - except (AstroidTypeError, InferenceError) as exc: - raise UseInferenceDefault(str(exc)) from exc + + if call.positional_arguments: + try: + first_value = next(call.positional_arguments[0].infer(context=context)) + except (InferenceError, StopIteration) as exc: + return nodes.Const("") + + if isinstance(first_value, nodes.Const): + return nodes.Const(str(first_value.value)) + + return nodes.Const("") def infer_int(node, context: InferenceContext | None = None): diff --git a/tests/brain/test_brain.py b/tests/brain/test_brain.py index 29c70a566..225f14f1a 100644 --- a/tests/brain/test_brain.py +++ b/tests/brain/test_brain.py @@ -1440,7 +1440,7 @@ def test_infer_str_const() -> None: assert inferred[4] == "False" assert inferred[5] == "None" assert inferred[6] == "4.33" - assert inferred[7] == "..." + assert inferred[7] == "Ellipsis" assert inferred[8] == "4" From 492f7d504f0f24d495773030f28180de0892b152 Mon Sep 17 00:00:00 2001 From: jkmnt Date: Thu, 5 Mar 2026 14:13:07 +0300 Subject: [PATCH 3/7] ok --- astroid/brain/brain_builtin_inference.py | 2 +- tests/brain/test_brain.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/astroid/brain/brain_builtin_inference.py b/astroid/brain/brain_builtin_inference.py index 716a84d80..810dba2be 100644 --- a/astroid/brain/brain_builtin_inference.py +++ b/astroid/brain/brain_builtin_inference.py @@ -852,7 +852,7 @@ def infer_str(node, context: InferenceContext | None = None) -> nodes.Const: if call.positional_arguments: try: first_value = next(call.positional_arguments[0].infer(context=context)) - except (InferenceError, StopIteration) as exc: + except (InferenceError, StopIteration): return nodes.Const("") if isinstance(first_value, nodes.Const): diff --git a/tests/brain/test_brain.py b/tests/brain/test_brain.py index 225f14f1a..9fc6d0c92 100644 --- a/tests/brain/test_brain.py +++ b/tests/brain/test_brain.py @@ -1419,6 +1419,7 @@ def test_infer_str() -> None: assert isinstance(inferred, astroid.Instance) assert inferred.qname() == "builtins.str" + def test_infer_str_const() -> None: ast_nodes = astroid.extract_node(""" str('') #@ From e9757cea05416d1bf922fe80e1c0a8af019756e4 Mon Sep 17 00:00:00 2001 From: jkmnt Date: Fri, 6 Mar 2026 16:48:45 +0300 Subject: [PATCH 4/7] handles the possible exception in str() call --- astroid/brain/brain_builtin_inference.py | 6 +++++- tests/brain/test_brain.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/astroid/brain/brain_builtin_inference.py b/astroid/brain/brain_builtin_inference.py index 810dba2be..58d8f169f 100644 --- a/astroid/brain/brain_builtin_inference.py +++ b/astroid/brain/brain_builtin_inference.py @@ -856,7 +856,11 @@ def infer_str(node, context: InferenceContext | None = None) -> nodes.Const: return nodes.Const("") if isinstance(first_value, nodes.Const): - return nodes.Const(str(first_value.value)) + try: + stringified = str(first_value.value) + except ValueError: + return nodes.Const("") + return nodes.Const(stringified) return nodes.Const("") diff --git a/tests/brain/test_brain.py b/tests/brain/test_brain.py index 9fc6d0c92..faf4a0a02 100644 --- a/tests/brain/test_brain.py +++ b/tests/brain/test_brain.py @@ -1407,6 +1407,7 @@ def test_infer_str() -> None: str(s) #@ str('a') #@ str(some_object()) #@ + str(7**10000) #@ """) for node in ast_nodes: inferred = next(node.infer()) From 40fdf2eee702c51caaebf0d549280849a5e01554 Mon Sep 17 00:00:00 2001 From: jkmnt Date: Tue, 31 Mar 2026 01:36:04 +0300 Subject: [PATCH 5/7] inferring all string values, not only the first one --- astroid/brain/brain_builtin_inference.py | 28 +++++++++++++++--------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/astroid/brain/brain_builtin_inference.py b/astroid/brain/brain_builtin_inference.py index 58d8f169f..9eec700da 100644 --- a/astroid/brain/brain_builtin_inference.py +++ b/astroid/brain/brain_builtin_inference.py @@ -849,20 +849,28 @@ def infer_str(node, context: InferenceContext | None = None) -> nodes.Const: if call.keyword_arguments: raise UseInferenceDefault("TypeError: str() must take no keyword arguments") - if call.positional_arguments: - try: - first_value = next(call.positional_arguments[0].infer(context=context)) - except (InferenceError, StopIteration): - return nodes.Const("") + fallback = nodes.Const("") + + if not call.positional_arguments: + return fallback + + # Accept only if all inferred values resolve to the same string + candidates: set[str] = set() + try: + for inferred in call.positional_arguments[0].infer(context=context): + if not isinstance(inferred, nodes.Const): + return fallback - if isinstance(first_value, nodes.Const): try: - stringified = str(first_value.value) + candidates.add(str(inferred.value)) except ValueError: - return nodes.Const("") - return nodes.Const(stringified) + return fallback + except InferenceError: + return fallback - return nodes.Const("") + if len(candidates) == 1: + return nodes.Const(candidates.pop()) + return fallback def infer_int(node, context: InferenceContext | None = None): From d6e686bc569cf27b0e4556ebda257cff5f0b6d9a Mon Sep 17 00:00:00 2001 From: jkmnt Date: Tue, 31 Mar 2026 01:44:22 +0300 Subject: [PATCH 6/7] bump the patch test coverage --- tests/brain/test_brain.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/brain/test_brain.py b/tests/brain/test_brain.py index faf4a0a02..b661f4495 100644 --- a/tests/brain/test_brain.py +++ b/tests/brain/test_brain.py @@ -1432,6 +1432,9 @@ def test_infer_str_const() -> None: str(4.33) #@ str(...) #@ str(2 + 2) #@ + str() #@ + str(int) #@ + str(2 if unknown() else 3) #@ """) inferred = list(node.inferred()[0].value for node in ast_nodes) @@ -1444,6 +1447,9 @@ def test_infer_str_const() -> None: assert inferred[6] == "4.33" assert inferred[7] == "Ellipsis" assert inferred[8] == "4" + assert inferred[9] == "" + assert inferred[10] == "" + assert inferred[11] == "" def test_infer_int() -> None: From b0b93c3753523349c3068548b3585facbef015db Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Fri, 22 May 2026 21:54:26 +0200 Subject: [PATCH 7/7] Address review feedback on str() inference - Add a ChangeLog entry for inferring str() of a constant argument. - Add a test ensuring a user-defined __str__ is never executed during inference: the argument infers to an Instance rather than a Const, so infer_str returns the empty-string fallback without running __str__. - Replace candidates.pop() with next(iter(candidates)) to drop the apparent source of indeterminism flagged in review (no behavior change). Closes #2994 --- ChangeLog | 7 +++++++ astroid/brain/brain_builtin_inference.py | 2 +- tests/brain/test_brain.py | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index df526b5c7..36428d124 100644 --- a/ChangeLog +++ b/ChangeLog @@ -68,6 +68,13 @@ Release date: TBA Closes #3067 +* ``str()`` of a constant argument now infers the actual string value instead + of always inferring ``""``. When every inference path of the argument + resolves to ``Const`` values that stringify to the same string, ``infer_str`` + returns that string; otherwise it keeps falling back to ``Const("")``. + + Closes #2994 + What's New in astroid 4.1.2? ============================ Release date: 2026-03-22 diff --git a/astroid/brain/brain_builtin_inference.py b/astroid/brain/brain_builtin_inference.py index 153755d70..3b275dce9 100644 --- a/astroid/brain/brain_builtin_inference.py +++ b/astroid/brain/brain_builtin_inference.py @@ -869,7 +869,7 @@ def infer_str(node, context: InferenceContext | None = None) -> nodes.Const: return fallback if len(candidates) == 1: - return nodes.Const(candidates.pop()) + return nodes.Const(next(iter(candidates))) return fallback diff --git a/tests/brain/test_brain.py b/tests/brain/test_brain.py index eaf2e9da6..5ab588130 100644 --- a/tests/brain/test_brain.py +++ b/tests/brain/test_brain.py @@ -1474,6 +1474,23 @@ def test_infer_str_const() -> None: assert inferred[11] == "" +def test_infer_str_does_not_run_user_str() -> None: + """A user-defined __str__ must never be executed during inference.""" + node = astroid.extract_node(""" + class StrWillFail: + def __str__(self): + raise RuntimeError + + str(StrWillFail()) #@ + """) + # The argument infers to an Instance, not a nodes.Const, so infer_str + # returns the empty-string fallback without ever calling __str__. + inferred = node.inferred() + assert len(inferred) == 1 + assert isinstance(inferred[0], nodes.Const) + assert inferred[0].value == "" + + def test_infer_int() -> None: ast_nodes = astroid.extract_node(""" int(0) #@