Skip to content

Conversation

@GDYendell
Copy link
Contributor

@GDYendell GDYendell commented Aug 16, 2025

Summary

Fixes #12734

I have started with simply checking if any arguments that are providing extra values to the log message are calls to str or repr, as suggested in the linked issue. There was a concern that this could cause false positives and the check should be more explicit. I am happy to look into that if I have some further examples to work with.

If this is the accepted solution then there are more cases to add to the test and it should possibly also do test for the same behavior via the extra keyword.

Test Plan

I have added a new test case and python file to flake8_logging_format with examples of this anti-pattern.

@GDYendell GDYendell force-pushed the logging-str-preformat branch 2 times, most recently from bdf1eab to 1de7082 Compare August 17, 2025 18:29
@ntBre ntBre added the rule Implementing or modifying a lint rule label Aug 18, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Aug 18, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+131 -0 violations, +0 -0 fixes in 3 projects; 52 projects unchanged)

apache/airflow (+41 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview --select ALL

+ airflow-core/src/airflow/dag_processing/bundles/manager.py:144:66: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ airflow-core/src/airflow/models/dagbag.py:134:49: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ airflow-core/src/airflow/utils/email.py:261:52: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ airflow-core/tests/integration/otel/dags/otel_test_dag.py:48:54: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ providers/amazon/src/airflow/providers/amazon/aws/hooks/dms.py:243:78: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ providers/amazon/src/airflow/providers/amazon/aws/hooks/dms.py:315:71: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ providers/amazon/src/airflow/providers/amazon/aws/hooks/dms.py:369:68: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ providers/amazon/src/airflow/providers/amazon/aws/transfers/s3_to_dynamodb.py:179:92: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ providers/apache/tinkerpop/src/airflow/providers/apache/tinkerpop/hooks/gremlin.py:150:75: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ providers/arangodb/src/airflow/providers/arangodb/hooks/arangodb.py:156:62: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ providers/arangodb/src/airflow/providers/arangodb/hooks/arangodb.py:167:62: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ providers/arangodb/src/airflow/providers/arangodb/hooks/arangodb.py:178:63: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ providers/arangodb/src/airflow/providers/arangodb/hooks/arangodb.py:189:62: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ providers/databricks/src/airflow/providers/databricks/operators/databricks_sql.py:391:98: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ providers/databricks/src/airflow/providers/databricks/operators/databricks_sql.py:426:100: RUF065 Unnecessary `str()` conversion when formatting with `%s`
... 26 additional changes omitted for project

apache/superset (+82 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview --select ALL

+ superset/annotation_layers/annotations/api.py:306:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/annotation_layers/annotations/api.py:380:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/annotation_layers/annotations/api.py:434:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/annotation_layers/api.py:158:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/annotation_layers/api.py:222:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/annotation_layers/api.py:293:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/charts/api.py:374:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/charts/api.py:450:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/charts/api.py:507:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/charts/api.py:785:70: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/commands/dashboard/importers/v0.py:79:36: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/commands/report/execute.py:613:76: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/commands/report/log_prune.py:54:25: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/commands/report/log_prune.py:55:25: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/daos/dataset.py:57:62: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/daos/dataset.py:92:68: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/dashboards/api.py:1324:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/dashboards/api.py:604:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/dashboards/api.py:690:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/dashboards/api.py:769:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/dashboards/api.py:851:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/dashboards/api.py:907:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/databases/api.py:2048:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/databases/api.py:2056:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/databases/api.py:482:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/databases/api.py:567:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/databases/api.py:624:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/datasets/api.py:1026:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/datasets/api.py:371:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/datasets/api.py:455:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ superset/datasets/api.py:511:17: RUF065 Unnecessary `str()` conversion when formatting with `%s`
... 51 additional changes omitted for project

zulip/zulip (+8 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview --select ALL

+ corporate/lib/stripe.py:5373:13: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ zerver/lib/email_mirror_server.py:68:75: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ zproject/backends.py:2068:13: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ zproject/backends.py:2844:54: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ zproject/backends.py:2966:64: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ zproject/backends.py:2988:65: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ zproject/backends.py:3004:65: RUF065 Unnecessary `str()` conversion when formatting with `%s`
+ zproject/backends.py:3076:77: RUF065 Unnecessary `str()` conversion when formatting with `%s`

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
RUF065 131 131 0 0 0

Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! This looks good to me overall, I mostly had small nit comments.

Otherwise, I'm not totally sure if we can make this a G rule. I see an upstream issue was opened last August, at the same time as the Ruff issue (globality-corp/flake8-logging-format#79). There hasn't been a response there specifically, but there has been activity in the repo in the last 3 months. The rule fits really nicely here with the other G rule implementations, as well as thematically, but we also don't want to run into rule conflicts down the line. @MichaReiser do we need to make this a RUF rule to be safe?

I'm also not totally sold on the name. What do you think about LoggingEagerConversion? Just an idea, I'm not sure I love that either.

Finally, would you mind looking through the ecosystem results? The simple implementation here is very appealing, but we'll want to make sure there aren't any false positives that might warrant parsing and cross-referencing the formatting arguments, as Charlie mentioned on the issue.

@GDYendell
Copy link
Contributor Author

GDYendell commented Aug 18, 2025

Thanks for the review! I added a couple of fixup commits to address your comments.

I'm also not totally sold on the name. What do you think about LoggingEagerConversion? Just an idea, I'm not sure I love that either.

I wasn't sure what to call it and chose the name in the hope that someone would come up with something better 😅

I prefer LoggingEagerConversion. It could also be LoggingEagerFormat or LoggingEagerCast?

Finally, would you mind looking through the ecosystem results? The simple implementation here is very appealing, but we'll want to make sure there aren't any false positives that might warrant parsing and cross-referencing the formatting arguments, as Charlie mentioned on the issue.

Will do - this is a really cool CI helper! I have had a quick look and there are some casts of exceptions, which possibly do more the logging formatter does.

@GDYendell
Copy link
Contributor Author

GDYendell commented Aug 18, 2025

I have had a look through the examples in the affected ecosystem packages and I don't see any issues. As a summary:

  • Exceptions in except blocks - implements __str__ and it is less work to just pass exc_info=True to the logging call with better results
  • Path objects - implements __str__
  • os.getpid() and other integers - implements __str__
  • apache superset Slice.to_json() - not sure where the implementation is, by imagine this returns a str
  • packaging Version - implements __str__

I didn't see any hits that aren't explicitly calling str or repr, so I think those are all correct matches.

@ntBre ntBre added the preview Related to preview mode features label Aug 19, 2025
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I had one more small suggestion, and then I did find a potential issue in the ecosystem check. A case like this is currently flagged:

import logging
try:
    1/0
except Exception as e:
    logging.error("error: %s", e)

That's no problem, the output is the same after dropping the str call:

>>> try:
...     1/0
... except Exception as e:
...     logging.error("error: %s", str(e))
...
ERROR:root:error: division by zero
>>> try:
...     1/0
... except Exception as e:
...     logging.error("error: %s", e)
...
ERROR:root:error: division by zero
>>>

However, mixing %s and repr, does change the result:

>>> try:
...     1/0
... except Exception as e:
...     logging.error("error: %s", repr(e))
...
ERROR:root:error: ZeroDivisionError('division by zero')
>>> try:
...     1/0
... except Exception as e:
...     logging.error("error: %s", e)
...
ERROR:root:error: division by zero

I saw this in this langchain example from the ecosystem check. That's not a super big problem given the current diagnostic message, which doesn't directly tell you to drop the repr call, but we'd at least need to add an example to the docs where %r is used.

However, the reverse combination of %r and str might actually be out of scope for the rule because it changes the output in a way that I don't think we can easily capture (abbreviated example to make comparison easier):

logging.error("error: %r", e)  # ERROR:root:error: ZeroDivisionError('division by zero')
logging.error("error: %r", str(e))  # ERROR:root:error: 'division by zero'
logging.error("error: %s", e)  # ERROR:root:error: division by zero

Note the quotes in %r/str compared to just %s.

I think that means we need to go ahead and do the format string parsing and comparison with the positional arguments that Charlie mentioned in the issue. We would need to do that for a future autofix anyway, although it's fine to leave a fix itself for a follow-up PR. What do you think?

This might be less of a big deal than I'm imagining, but flagging cases where users need to change "%r", str(...) to "'%r'", ... feels like it could be confusing and less desirable compared to cases where you just drop a str or repr call.

Otherwise I think we just need to decide on the name and rule code!

@GDYendell
Copy link
Contributor Author

A case like this is currently flagged:

import logging
try:
    1/0
except Exception as e:
    logging.error("error: %s", e)

Is your first snippet here supposed to have str(e) rather than e, or is it actually flagging a case where it doesn't use str or repr?

However, mixing %s and repr, does change the result:

try:
...     1/0
... except Exception as e:
...     logging.error("error: %s", repr(e))
...
ERROR:root:error: ZeroDivisionError('division by zero')

Ah that is interesting, I hadn't appreciated that. This is a tricky one then because the user might prefer that output. They would want to use logging.error("error: %r", e) to get the same, so we need to change the suggestion depending on whether str or repr was used. If you are happy to leave that until another PR, for now maybe it would be useful to say something like "use %s to format with str or %r to format with repr".

The %r & str() case is an odd one - would that really be intentional? I am not sure what the suggestion should be because you can't get the same output with just a format string. It is relying on casting to a str and then formatting the repr of a str, which wraps it in quotes. This doesn't give the same output:

try:
   1/0
except Exception as e:
   logging.error("error: %r", str(e))

ERROR:root:error: 'division by zero'
try:
   1/0
except Exception as e:
   logging.error("error: '%r'", e)

ERROR:root:error: 'ZeroDivisionError('division by zero')'

@ntBre
Copy link
Contributor

ntBre commented Aug 19, 2025

Is your first snippet here supposed to have str(e) rather than e, or is it actually flagging a case where it doesn't use str or repr?

Ah yes, you're right, sorry!

Ah that is interesting, I hadn't appreciated that. This is a tricky one then because the user might prefer that output. They would want to use logging.error("error: %r", e) to get the same, so we need to change the suggestion depending on whether str or repr was used. If you are happy to leave that until another PR, for now maybe it would be useful to say something like "use %s to format with str or %r to format with repr".

I think I'd prefer to go ahead and give a tailored message for each case we know about, if it's not too much more work. I ran into a somewhat analogous case for another rule recently where we got reports about the confusing, generic help message. This looks like an example of similar parsing in another rule that doesn't look too bad:

// Parse the format string (e.g. `"%s"`) into a list of `PercentFormat`.
let Ok(format_string) = CFormatString::from_str(string) else {
return;
};
if !convertible(&format_string, right) {
checker.report_diagnostic(PrintfStringFormatting, string_expr.range());
return;
}
// Count the number of positional and keyword arguments.
for (.., format_part) in format_string.iter() {
let CFormatPart::Spec(fmt) = format_part else {
continue;
};
if fmt.mapping_key.is_none() {
num_positional_arguments += 1;
} else {
num_keyword_arguments += 1;
}
}

The %r & str() case is an odd one - would that really be intentional? I am not sure what the suggestion should be because you can't get the same output with just a format string. It is relying on casting to a str and then formatting the repr of a str, which wraps it in quotes. This doesn't give the same output:

Oops, good catch again. Yeah, we might want to avoid a diagnostic in this case. It seems like kind of a roundabout way to wrap something in quotes, but I still think it's intentional. We could always expand the rule later, but I'd lean more towards definite true positives like %s paired with str for a first draft.

I also remembered that there's a vaguely similar rule for f-strings: explicit-f-string-type-conversion (RUF010). This replaces calls like f"{repr(a)}" with f"{a!r}". Most notably, it applies to str, repr, and ascii. I think we could apply this rule to ascii and %a as well, but I think that's definitely the least commonly used of the three. Just another idea.

@GDYendell
Copy link
Contributor Author

GDYendell commented Aug 19, 2025

OK I think I see how to do that. Just to confirm, we want to iterate over the placeholders in the format string with the corresponding values and only flag %s with str() and %r with repr() as issues, ignoring the two inverted cases for now?

@ntBre
Copy link
Contributor

ntBre commented Aug 20, 2025

OK I think I see how to do that. Just to confirm, we want to iterate over the placeholders in the format string with the corresponding values and only flag %s with str() and %r with repr() as issues, ignoring the two inverted cases for now?

Yep, that's exactly what I had in mind!

@GDYendell
Copy link
Contributor Author

GDYendell commented Aug 20, 2025

It now only flags %s with str() and %r with repr(). What do you think?

Is it possible to provide two ranges to the diagnostic so that it underlines both %s and str()?

@ntBre
Copy link
Contributor

ntBre commented Aug 21, 2025

It now only flags %s with str() and %r with repr(). What do you think?

That looks great!

Is it possible to provide two ranges to the diagnostic so that it underlines both %s and str()?

Yes, as of ~last week! The easiest way is with secondary_annotation on the guard returned by report_diagnostic:

/// Add a secondary annotation with the given message and range.
pub(crate) fn secondary_annotation<'a>(
&mut self,

Here's the one use of it we've added so far:

diagnostic.secondary_annotation(
format_args!("previous definition of `{name}` here"),
shadowed,
);

And an example of the type of diagnostic it produces (the ----- underlining is used for secondary annotations):

F811 Redefinition of unused `bar` from line 6
--> F811_0.py:10:5
|
10 | def bar():
| ^^^ `bar` redefined here
11 | pass
|
::: F811_0.py:6:5
|
5 | @foo
6 | def bar():
| --- previous definition of `bar` here
7 | pass
|

There are some other options if that doesn't look good, but that's the easiest place to start.

@GDYendell
Copy link
Contributor Author

Hmm. I am not sure this is possible because Expr::StringLiteral only has a range for the entire string. Unless we wanted the secondary diagnostic to underline the whole string, which I am not sure adds much:

G005 Unnecessary cast `str()` in logging values when formatting with `%s`
 --> G005.py:4:14
  |
3 | # %s + str()
4 | logging.info("Hello %s", str("World!"))
  |              ----------  ^^^^^^^^^^^^^
  |              |
  |              Logging message here
5 | logging.log(logging.INFO, "Hello %s", str("World!"))

Thoughts?

@ntBre
Copy link
Contributor

ntBre commented Aug 29, 2025

Yeah, I don't think it makes sense to underline the whole string. I guess I was assuming that CFormatString would have a range for each part, but it doesn't look like it does. No worries, and thanks for looking into it! A single diagnostic range seems fine.

Would you mind resolving the conflicts? Hopefully they're not too bad. I'll try to give this another review soon :)

@GDYendell GDYendell force-pushed the logging-str-preformat branch from e5547d4 to ab44c73 Compare August 29, 2025 19:39
@GDYendell
Copy link
Contributor Author

Squashed and rebased to resolve conflicts.

Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! This looks great to me. I just had a few very small suggestions, and then I think we should move this to a Ruff rule. But this is otherwise good to go.

@GDYendell GDYendell force-pushed the logging-str-preformat branch from f6665ec to e8b6439 Compare September 11, 2025 19:34
@GDYendell GDYendell changed the title Add rule G005 for redundant string pre-formatting in logging calls Add rule RUF065 for uneccesary string conversion in logging calls Sep 11, 2025
@GDYendell GDYendell force-pushed the logging-str-preformat branch from e8b6439 to 229369c Compare September 11, 2025 19:40
@GDYendell GDYendell changed the title Add rule RUF065 for uneccesary string conversion in logging calls Add rule RUF065 for unnecessary string conversion in logging calls Sep 11, 2025
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent, thank you! I managed to find a few more tiny nits, but this looks great.

@GDYendell GDYendell force-pushed the logging-str-preformat branch from 20d436a to 427ad1c Compare September 19, 2025 20:12
@ntBre ntBre changed the title Add rule RUF065 for unnecessary string conversion in logging calls [ruff] Add logging-eager-conversion (RUF065) Sep 19, 2025
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again!

@ntBre ntBre merged commit 44fc87f into astral-sh:main Sep 19, 2025
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview Related to preview mode features rule Implementing or modifying a lint rule

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rule Request: redundant logfmt str ctor catch in G rules or add new rule

2 participants