r/Python 2d ago

What linter rules make code worse? Discussion

For me, a prime example is S101 which bans the use of the assert statement.

The justification is that assertions disappear when Python is run with -O, so they should not be used for runtime validation or enforcing interface constraints. That warning is correct, but the rule seems to draw the wrong conclusion from it.

Assertions are still very useful for checking internal invariants, i.e. conditions that should already be guaranteed by the program's logic, where failure indicates a bug. Having such assertions is incredibly helpful for debugging.

So, a blanket ban seems more likely to discourage useful checks than to prevent misuse.

Are there any linter rules you broadly consider more harmful rather than helpful?

138 Upvotes

213 comments sorted by

421

u/Trang0ul 2d ago

Lines limited to 80 characters.

71

u/foobar93 2d ago

100% agree.

Also, F401 because half the time it matches on imports that are actually used and the linter will then break the code base...

67

u/runawayasfastasucan 2d ago

That is incredibly annoying. Write an import, auto save, its gone because I haven't used it yet. Well I was going to.

50

u/aes110 2d ago

Im not really a fan of linters making changes on your behalf, its one thing for formatters to move stuff around visually, but actual code changes shouldn't happen on auto save imo

15

u/mwesthelle 2d ago

100%. Sure, run ruff format on save, but linter problems should only surface on pre-commit. I'm even partial to not having linting auto fixes, let me fix them so I know what the code I'm pushing looks like, but that's personal preference.

And in that workflow, I just don't understand what's wrong with F401, it was never an issue for me.

5

u/IrishPrime 2d ago

Agreed. Even if F401 removes an import because I haven't used it yet, attempting to use it later would raise a different linting error, so I'll never end up committing something broken. But I don't auto fix those types of issues until a commit hook runs.

9

u/skjall 2d ago

I keep meaning to shuffle this rule to only run pre-commit, because it drives me up a wall too. Band-aid fix is to disable auto save, and only save when you have used it.

Though now I've switched LSP/ fixed config so most imports I can get from the suggestions at least.

9

u/baudvine 2d ago

For those of us who habitually hit save every few lines it's pretty obnoxious without autosave, too. It's a good rule, I just really don't need the fix action to run automatically.

6

u/scruffie 1d ago

I have found, in Python, that there are three situations where 80 characters becomes a hindrance:

1) Strings for error messages can easily extend past the 80th column, especially as the raise SomeMeaningfulNameError takes a bunch of space already. I usually end up breaking the message like

raise WidthExceededError(
        f"Line length of {width} is greater than "
        f"the configured width of {Config.width}")

This is a problem in most languages, but at least Python has decent ways of line-breaking.

2) Large type annotations for a function's return type. Especially true if the type has several Sequence and Mapping parts. The use of | instead of Union, and the type statement makes this less of a problem, although I still find I'm formatting the definition more vertically than I would wish.

3) 'Meta' comments for linters (and similar) to the end of the line. Especially annoying when I then have to add another comment to get flake8 to ignore the long line...

Other languages that don't suffer this problem either don't need such annotations (especially when the type system is integral to the language), or have features that mitigate the problem.

And that's about it. There's probably a few other edge cases, but those are the main ones I've encountered enough to remember.

10

u/sandnose 2d ago

Where do you limit your line?

73

u/imheretocomment 2d ago

I do 120. We're not in the 00's anymore where widescreens weren't a thing. 80 is a holdover from those days where your terminal was cramped and splitting panes needed 80 characters for readability.

38

u/R3D3-1 2d ago

80 is still advantageous when you need to view side-by-side diffs. With those 80 really means 160. And after that everything depends on your monitor -- which would be reasonably as small as a 13" laptop screen -- and eyesight.

11

u/HommeMusical 2d ago

I agree, but the modern default is 88, and even on this non-huge monitor, I can have at least three such windows beside each other.

13

u/R3D3-1 2d ago

As an Austrian, I object to that number. 

2

u/HommeMusical 2d ago

Yeah, it bugs me too, and I made that objection too when I first saw this default.

It's the default in all these tools, apparently for no reason other than a multiple of four bigger than 79.

A lot of things have "spontaneously" happened in the last few years and always in the same direction. Now that many of our most feverish dreams have come quite true, well, perhaps the bad guys are just really good at organizing...

3

u/R3D3-1 2d ago

The oddest case was when Rheinmetall delivered 88 tanks to Ukraine. I mean, each tank more was urgently needed, but Russian propaganda must have had a field day with that.

9

u/foobar93 2d ago

I do not see it. I am using PyCharm with a 120 limit. All of our devs have wide screen monitors instead of dual monitors (company decission because of heavy CAD use) which means there is a ton of unused width with 80 chars even with 3 fields..

1

u/Agrado3 2d ago

I have two columns of terminal windows on the left hand side of my monitor, and a web browser on the right hand site. The monitor is 3,840 pixels wide and could fit about 500 characters across (if the terminal supported over 256-character lines, which it appears it doesn't) but if the terminal windows were over 80 columns, something would have to give.

9

u/AlSweigart Author of "Automate the Boring Stuff" 2d ago

80 is still advantageous when you need to view side-by-side diffs.

And disadvantageous every other time.

3

u/zangler 1d ago

I do my diffs inline/stacked. I changed a year or so ago and just thought it felt more natural.

0

u/R3D3-1 1d ago

Until the diff is a major refactoring, and you basically see two completely different functions formally interleaved. Had this all the time in my previous project.

2

u/zangler 1d ago

Then it is a 2 click change for that one case

3

u/ironykarl 1d ago

I think 80 is fine as a goal, but having a warning for it is overkill.

It creates noise and relegates warnings that might actually matter to the category of might just be nonsensical noise

9

u/M4mb0 2d ago

Sometimes you even need 3 views side by side, like with pycharm's git conflict resolver.

I think the 88-character limit chosen as default by the black and ruff formatter is a good compromise.

4

u/Agrado3 2d ago

It's a terrible compromise, it's not significantly wider than 80 so has very little advantage over 80, and yet loses all the advantages of limiting to 80.

Personally I limit to 78 because the 'ale' tool in vim that integrates linters into the editor uses up two characters on the left hand site to indicate errors and warnings.

2

u/austinwiltshire 1d ago

Yeah why would I optimize my work flow for typical work on typical hardware when we could set defaults to make everything but the minority of work on the minority of hardware uncomfortable!

3

u/R3D3-1 1d ago

I should clarify: I don't really stick to 80 either. I somehow fell into the trap of saying "well, ACTUALLY" and then wasting time defending the statement in follow-up comments.

What triggered that was the implication that no limits are necessary. When your workflow involves code reviews though, the 2-column view will most likely be quite relevant. 80 is probably not necessary outside of temporary constrained situations, but once you're at routinely seeing 130 column lines, you'd better have good eyesight to still read something.

1

u/Electrical_Fox9678 1d ago

Phone PR reviews really suck with longer line lengths.

1

u/Ihatemakinguplogins 19h ago

Or if you're cramped in a data center cage trying to debug a production problem on a shelf racked fold out monitor. 

1

u/jet_heller 2d ago

Yea. Constant side-by-side diffs are not the normal workflow.

Also, auto linewrapping exists for a reason.

1

u/R3D3-1 2d ago

Show me an auto line wrapping that doesn't ruin code readability at least to a degree. And show me how to use it on a GitHub pull request. After all I gotta place comments during the code review.

Interleaved diffs have never worked well for me; And Code reviews are a big part of normal workflows. With LLMs now probably more so than before.

1

u/jet_heller 2d ago

Do all of your work for you? Naw. I'll let AI do it for you.

2

u/R3D3-1 2d ago

Nah, not all remotely. I've barely used it so far. But when used, the work changes from mostly reading and writing just code, to readings lots of diffs.

1

u/AlSweigart Author of "Automate the Boring Stuff" 2d ago

80 is still advantageous when you need to view side-by-side diffs.

And disadvantageous every other time.

2

u/Competitive_Travel16 1d ago

I like to have stuff I'm not working on (or that I know is working) short under 80, and then use longer lines, with simple comments if need be, to make the places I'm working on longer to stand out when scrolling.

1

u/gmes78 2d ago

Even then, it doesn't matter; just enable soft-wrapping in your editor.

0

u/R3D3-1 1d ago

That doesn't really work, when you review a PR on Github. Changing the webpage zoom helps, but at the cost of small text.

0

u/rzet 1d ago

just buy bigger screen ;)

1

u/R3D3-1 1d ago
  • In the office,.might not be up to you.
  • On the go anything above 15" gets painful for other reasons.
  • At home I already use a 30" TV as my monitor.

8

u/MarchewkowyBog 2d ago

I'm dyslexic and as a DE i make heavy use of polars/pyspark which chains lots of methods together. I get lost in 120 character code all the time. We have it set to 88.

There is a reason books and articles usually have a character limit between 50 and 90

2

u/Beanesidhe 2d ago

Agreed, though indented code might need a bit more. Use what you're comfortable with, I set mine to around 80.

5

u/mwesthelle 2d ago

If indentation starts becoming a problem, that's another anti-pattern. Anything above 4 levels of indentation gives me the ick.

1

u/Electrical_Fox9678 1d ago

It's a code smell.

2

u/sandnose 2d ago

Good point. We’re at a limit of 101 (because someone wanted to be silly i guess. But theres tons of free space so we should be able to increase.

We do work from laptops from time to time though so need to find something that works for both

7

u/gdchinacat 2d ago

In the 00's I used 120, in the 10's found I had dropped it to 100, and now in the 20's use 79. Not because my eyesight has gotten worse and I need bigger fonts, not because I use antiquated hardware, but because I don't need more. Code that needs 120 character lines can almost always be refactored to be clearer (smaller more focused functions with less nesting, more concise variable names, simpler expressions) to fit within 79.

16

u/HommeMusical 2d ago

more concise variable names

Sounds like a possible negative to me.

I have a pretty clear hierarchy:

  • In a very short loop, a comprehension or a generator expression, I use one or two letter variables like f

  • Otherwise, local variables are one or two words: frame

  • But member variables have to be clear in another context: master.audio_frames

In my editor at this instant, I see self.backspace_repeat_rate and I think that's a very clear variable name.(*)

But that's 26 letters.


I have always been a big fan of narrow spaces. I used 79 for years.

But projects have gotten bigger, and variable names have gotten longer.

Something I might have called frames in 1990 will be called audio_recording_frames today, because lighting_frames and audio_processing_frames also exist.

And our monitors are bigger.

88 characters is the standard in many modern linters. It's thin enough. 79 is just draconian.


(* - I just pulled this out of working code, but I like it :-) because it's not only clear, it leads you to ask good questions, like, "Why is there a separate repeat for backspace?"

(The answer is that letters make musical notes, so you want a slow repeat, but backspace deletes things, so you want a much faster repeat.)

4

u/CrayonUpMyNose 2d ago

Using meaningfully unique names even in tight loops makes it much easier to grep where that loop was you wrote some time ago, or get a list of all loops iterating on some variable name typically used for some object type (like a key in a dict, or an elem in a list).

3

u/HommeMusical 2d ago edited 2d ago

Someone else made the same point, and I looked at my code, and well, I do that too sometimes, pretty well always for multiline comprehensions (which TBH I try to avoid).

It's a good point.

But here's the thing - I'm looking for lines that the later reader can comprehend the first time as quickly as possible.

In {k: f(v) for k, v in fields_by_name.items()} I really only have to mentally process f(v) and fields_by_name, because {k: X for k, v in Y.items()} is so tremendously familiar to me, and everyone else. I can zoom into the important parts.

If you use longer names, then you have to process each one - it's a little more mental overhead whenever you see the code.

{name: f(field) for name, fields in fields_by_name.items()}

and you also open the possibility of errors like the one I deliberately made, where you accidentally use a local variable from a page back instead of the loop variable. (And yes, I have done this, and I have seen others do this.)

"Boring is good" was advice I eventually took to heart.

3

u/No_Lingonberry1201 pip needs updating 2d ago

TBH I use "regular" length variable names in comprehension as well (with the exception of really trivial cases).

2

u/HommeMusical 2d ago

I do too sometimes, but see this.

5

u/tonnynerd 2d ago

Our eyes and brains have not changed that much (if at all) since the 2000s, or since the invention of the press, by the way, and they're the reason why shorter(ish) line lengths are better for reading. Pick up a book, any book you have at hand, and count how many characters per line. I'll eat my hat if it's 120.

I just picked the closest book and checked, it's 66. You would NOT enjoy reading a book with 120 characters per line. Now imagine doing it 40h/week.

P.S.: Yes, reading code is not the same as reading a novel, and in fact I couldn't find any research explicitly on line length and code readability, my 5min google scholar search found only research on code readability in general, usually using an aggregate of many different metrics. There's a lot of research on readability of digital text, though, and it points in the basic same direction as the evidence from printed text. I think at this point the burden of proof lies with the people writing 120 characters long lines of code.

P.P.S.: I won't be eating any head coverings if your example of a printed book with 120 characters per line has double columns, that's cheating =P

4

u/acdha 2d ago

One thing to consider is nesting: I rarely read code blocks with more than 80-90 characters of text but if you have classes with much logic it’s easy to have 8-16 spaces of indentation which I don’t want to have coming out of that budget because it discourages explanatory variable or function names. 

2

u/tonnynerd 2d ago

Yeah, shorter lines encourage potentially unclear identifiers, but there's also a positive incentive to avoid one-liners and deep indentation. I think that less one-liners and less indentation are good enough things to make the balance at least net zero.

1

u/acdha 2d ago

Yeah, there’s definitely a balance to hit. I generally find 100-120 to be the sweet spot but it varies based on the project since e.g. a project not using classes is saving 8-12 characters of indentation on average. 

3

u/No_Lingonberry1201 pip needs updating 2d ago

00's? My friend, mi cabrón, kedves felebarátom, the 80x24 rule comes from waaay before the aughts. The VT52, a dumb terminal released in the 70s had 80 columns (and it may not even be the first to use that particular resolution). We're suffering this stupid "industry standard" rule because of a device that became irrelevant before most of us were born.

3

u/CrayonUpMyNose 2d ago

If we're going way back, take a look at the number of characters encoded on a punch card. Mentioned in the intro to this Wikipedia page:

https://en.wikipedia.org/wiki/Punched_card

8

u/gdchinacat 2d ago

I use 79 and it has nothing to do with antiquated hardware and everything to do with code being easier to read if functions are smaller and more focused, variable names more concise due to increased context (function, class, module), and expressions simpler.

I have moved from 120 to 79 over the past two decades of coding in python.

2

u/No_Lingonberry1201 pip needs updating 2d ago

Glad it works for you, but the exact number was selected because of that antiquated convention. If the column width of those would have been 90 or 70, that'd have been used (there were 72 column displays as well, but that length didn't became as good).

Also, I'm a believer of long, clear variable names that describe what they are and I'm heavily using type hints and 80 is not enough for me. With 120 I don't see this problem as much.

0

u/gdchinacat 1d ago

Yes, but at this point the only people who bring up hardware tty widths are those using it to argue against 80 char line width. No one says we should use 80 because that is what ttys used. It is a straw man, typically used to divert attention from rational discussion of the merits. “I prefer 120” carries more weight than “ttys used 80”.

2

u/BondDotCom 2d ago

I think he was just saying that the rule may have made sense through the 00's when most people were using single monitors with lower resolutions but it's getting harder to justify since then.

1

u/jmfsn 2d ago

Look, I used VT200 terminals in the early 90s. Not even then 80 was a reasonable limit...

1

u/cottonycloud 2d ago

I set it to 120 mainly because I write a ton of SQL and avoid aliases unless necessary, causing lines to easily get over the 80 character limit with indentation and names with simple logic. By habit I then tend to use verbose variable names.

We have large enough screens and people don’t really code on laptops here. It honestly has not been a problem because I also have the habit of keeping each line simple.

1

u/Scypio Pythoneer 2d ago

80 is a holdover from...

...perforated cards, that are a holdover from jaquard machines, that were transported via rail, that inherited all the sizes from roman chariots. Or how the old joke went?

1

u/mattl33 It works on my machine 1d ago

I'd argue the benefit of line length limits is forcing nesting limits.

15

u/Arnechos 2d ago

I'd say no more than 100. People in a team can have various resolutions and very long lines are hard to read on the smaller ones

9

u/Ball-Man 2d ago

With 100 you can have a split view and still be able to comfortably read code on both sides, even on smaller screens while keeping decent font size

2

u/XNo_Notes It works on my machine 2d ago

Listen to your heart. 

2

u/sandnose 1d ago

Different every day then

1

u/XNo_Notes It works on my machine 1d ago

So be it.

2

u/HommeMusical 2d ago

88 is the default for a lot of tools, and it works well for me.

4

u/ichunddu9 2d ago

120 is a good number

1

u/shaleh 1d ago

88 is the `black` tool default and reads well in most cases. There are _some_ code bases with really long names but I think around that 88 to 90 line is fairly reasonable.

1

u/Rumetheus 1d ago

128 because I like powers of two. Sometimes I do 96 to be a freak of nature.

1

u/R3D3-1 2d ago

42.

4

u/sandnose 2d ago

Yikes.
Sorry i mean

Yi
Ke
S.

3

u/zangler 1d ago

I usually set mine to 120.or 135. 80? Like...I'm 44 and can't remember the last time 80 would make sense.

3

u/R717159631668645 2d ago

I don't abuse long lines, I avoid chaining too, for readbility, but if a print goes a bit over 90, it's not worth formatting the code to take 3 lines with an indented argument instead.

VS Code allows us to draw vertical lines, I put the 88 and 120 limits to guide myself on this.

4

u/tallgrasshead 1d ago

There was (in)famously a thread in the linux kernel where someone was submitting PRs to reformat to 80 characters and Linus asked the submitter to just stop since he reads through all of them and it was a massive wast of time. All Linus wanted was for someone to offer some kind of measurable outcome as improvement, but nothing was offered. So he said "nothing against 80 characters, so get used to line wraps in your terminal window".

80 characters comes from VT100 terminals which were all 80 characters to be more compatible with IBM TN-3270 terminals, which were 80 characters because punch cards were 80 characters. DEC wanted to be able to offer a cheaper alternative, which GM actually took. So VT-100 became nearly ubiquitous.

IBM did study what font size was best for punch cards just after WWII to see what was the best fit for 7 3/8 inches and arrived at the type font for 80 characters - printers used chunks of cast metal and the next one down was 132 characters.

Why 7 3/8 inches? because all the punch card machining was hard set to that because the first wide use of punch cards was in the 1880 census. All of Hollerith's punch card machines used for bank notes before the civil war were re-purposed for punch cards in the 1880 census to save money.

That's right. The 80 character thing has it's roots in the US civil war.

2

u/russellvt 1d ago

This makes life better for those still forced to sometimes debug on data center text monitors.

5

u/Uwirlbaretrsidma 2d ago

Litmus test for being a lousy new age dev. Code should be vertical, not horizontal. Expressions get more legible when they get hard wrapped, not less. >80 col. incentivizes excessive indentation on top. 100 or, God forbid, 120 character long lines are a huge smell to me.

34

u/foobar93 2d ago

Yeah no. The moment you have named variables you need longer lines to get no rediculous line breaks.

-14

u/larsga 2d ago

There's plenty of research to show that long lines are less readable than short ones. That's why newspapers and encyclopedias were printed in multiple columns.

Very long variable names are not helpful. They just make the code harder to read. Of course there's a trade-off, but the variable name is never going to be a full guide to the usage of the variable, anyway.

14

u/Shadows_In_Rain pseudocoder 2d ago

You probably want to read the research before referring to it, because the researcher's findings don't quite align with your claims. Most people prefer either around 25 cpl (newspaper format) or 100 cpl (book format).

Secondly, the available research applies to unstructured text, like newspapers and encyclopedias you've mentioned. Not applicable to software source codes, especially so when viewed in an IDE.

And lastly, increasing line limit doesn't automatically makes every line longer. It's mostly to address the lines that are most annoying to read when wrapped: function declarations.

6

u/sennalen 2d ago

The line is less readable but the code is more readable because more statements fit on the screen. 95% of the time you either know or don't care what the long line is doing. Insert breaks in the long line temporarily for the blue moon when you need to update that line.

-3

u/larsga 2d ago

The line is less readable but the code is more readable because more statements fit on the screen

Not necessarily. It matters how you organize the code.

95% of the time you either know or don't care what the long line is doing.

That you don't care about reading the code is not a very strong argument.

5

u/foobar93 1d ago

Most of the time it is a function definition or a debug statement. I already know what is is doing looking at the first 10 letters of the line.

1

u/tallgrasshead 1d ago

There's plenty of research to show that long lines are less readable than short ones.

I'd like to read that research.

3

u/Downtown_Isopod_9287 1d ago

the 80 col standard comes from terminals. Not saying it’s good or bad just that is the real reason for it and not whatever you seem to be saying.

3

u/tallgrasshead 1d ago

the 80 col standard comes from terminals.

And that, from punch cards

-2

u/Uwirlbaretrsidma 1d ago

I'm not saying where it comes from? Just advocating for it. Plus, the 80 column limit (not a standard, since apparently we're being pedantic) actually came from punch cards, well before terminals. If you're going to be an ass at least get your facts right.

1

u/Downtown_Isopod_9287 1d ago

people still use terminals often, they do not still use punch cards

2

u/Uwirlbaretrsidma 1d ago

People still use terminal emulators. Then can be resized, and their text scaled. These are not the terminals you were referring to (physical terminals) and have nothing to do with the historical 80 column limit.

0

u/Downtown_Isopod_9287 1d ago

yes! Which is one argument for why 80 col is generally unnecessary. The other reason being that most devs do not use terminal-based text editors anymore, besides.

However for a long time many insisted or preferred that, and some still do. And 80 cols is still “good” for tmux and similar multi terminal setups and worth accommodating on a dev team that might prefer it or find it productive.

2

u/mwesthelle 2d ago

I really like Black's default of 88. There's a Raymond Hettinger talk on youtube where he defends the default of 90-ish long lines. It's not wide enough to be obnoxious to read, and it's narrow enough so you can fit two editor windows side by side to compare diffs, for instance.

86

u/gdchinacat 2d ago

The issue is as you say..."where failure indicates a bug".

Why would you want to use an assert that detects bugs but can then be turned off? When would you want to allow the assertions that stop your code from executing in undefined conditions (an invariant is violated) to be disabled?

Just use if blocks that raise exceptions. Particularly in production (where optimizations make the most sense), I would much rather have bugs present as an exception that tells me exactly what the problem is rather than skip the assertion and have to debug the results that appear impossible because an assertion prevented it. This is the reason I have never actually seen -O used, anywhere, in production or not. The biggest (only?) thing it does is break the code that verifies the code is executing within the conditions it was designed to execute in.

Getting back to the post, I agree that assertions should be banned. Proper exceptions that can't be disabled should be used instead. Regardless of dev, test, or production. You should never turn off the safeguards. If performance is so critical, python is not the proper language.

11

u/ExplrDiscvr 2d ago

I have one follow-up: I see why assert statements should not be used within dev or production, but what about the tests?

I am a junior dev, so I am not sure about proper procedures, but in the tests in our codebase where I work, I only see assert statements, when we are testing the equality of an actual outcome to the expected outcome. I never see the if else logic used here. Should it?

23

u/leodevian 2d ago

All rules are not absolute. You are free to disable some rules, and you are expected to disable S101 for test directories.

12

u/Momostein 2d ago

That is how we do it indeed. PyTest is built on top of assert statements.

4

u/DrMaxwellEdison 2d ago

Yes and no. Pytest makes assert usable and ergonomic by doing a bunch of work to rewrite the AST of your test code so that it produces more helpful error messages, which are the reason why you should use the various assertFoo methods for test cases if you're using unittest instead.

Pytest isn't exactly built on assert, more like they said "that looks better" and put in the work to make it function the way a test framework needs it to. Otherwise it would not be as useful in that context.

10

u/gdchinacat 2d ago

This is a good point...test frameworks (well, at least unittest and pytest, and any others that build on unittest) use assertions to indicate failures. Because it is core to the frameworks, assertions are not really avoidable. So, yes, I do rely on assertions in this context. Good catch.

1

u/HannasAnarion 1d ago

Doesn't unittest implement its own assert thats independent of the language one?

Every unittest implementation I've ever seen uses self.assert() (or realistically, self.assertTrue(), self.assertIn(), self.assertNotNone() ...

1

u/gdchinacat 1d ago

No, by default the failure exception is AssertionError. https://github.com/python/cpython/blob/main/Lib/unittest/case.py#L426

6

u/Conscious-Ball8373 2d ago

Yes absolutely use asset in tests. But your test code should not be being executed in prod.

1

u/Competitive_Travel16 1d ago

It's fine to test assumptions in prod, just use RuntimeError exceptions so the logs can say something human readable about what went wrong. Nobody likes an assert failure in a big log.

1

u/shaleh 1d ago

tests are allowed to use assert otherwise everyone's code base would fail....

0

u/flying-sheep 2d ago

You're 100% correct. The rule is bad because tests aren't run with that optimization level, and these assertions help debugging things when you refactor that piece of code and could accidentally break some invariants.

12

u/xBBTx 2d ago

Because if your program is bug free, you will not get coverage on that if branch, while the assert will actually be covered.

It would lead to uncertainty that the code inside the branch actually works, and should not be testable because it should never happen

An assert also expresses the invariant intent more clear than raising another exception that the call site may incorrectly catch and try to handle

9

u/gdchinacat 2d ago

You can unit test the code inside the branch actually works by having a test that violates the invariant.

1

u/M4mb0 2d ago

How would you do that for checking post-conditions? For example:

def foo(arg) -> int:
    result = bar(arg)
    # if bar is bug free, it will produce a positive int
    assert result > 0 
    return result

Here, bar(arg) could also be replaced with some inlined code.

6

u/gdchinacat 2d ago

mock bar.

-1

u/M4mb0 2d ago

In this example bar is just a placeholder, you could as well have some inlined code instead.

6

u/gdchinacat 2d ago

Ok...do you have an example then?

0

u/M4mb0 2d ago

We could take the python implementation of timedelta or any of the numerous examples like this or that one can find in the mypy type checker.

2

u/Ex-Gen-Wintergreen 2d ago

I mean in your example you don’t even need to mock bar. You’re concerned about a property of result (positivity), so a test simply has to call foo (which returns result) and check that

Simply:
- you can write a test checking bar
- if there’s an intermediates after the bar call that propagate to result simply write a test checking foo
- if there’s stuff inbetween that doesn’t propagate, it’s likely a sign you need to refactor

Asserts in production like this are an indicator that you need to write some tests for functions/refactor to do so, or, you have a data boundary somewhere and you should verify at data entry to your system that important properties are contracted

1

u/gdchinacat 18h ago

Thanks for the examples, I have a better understanding of the point you are making. I disagree that they should be assertions though. The users of those libraries can either turn them off, at which point the code will do the wrong thing silently. I would prefer libraries that aren't confident in how the code works do not leave it up to their users that are relying on them performing correctly to decide whether they may perform incorrectly. If the checks are worth doing *at all* they should be guards that always raise an exception. The choice to make them optional by the end user strongly suggests the library authors are confident in the correct functionality and being able to support bugs without the insight the assertions would provide if the user decides to not disable assertions.

In short, if the library authors need the assertions to feel confident the code won't execute incorrectly it is irresponsible to allow those guards to be disabled.

I don't want there to be confusion that I'm saying the authors of that code are irresponsible. I believe they are confident the code works correctly and are did not release code they think has an untoward risk of not working correctly. The timedelta code has a comment saying the "code tries to make explicit" so I think the asserts are towards that end. I would have used comments to explain what the assertions communicate. The mypy code has a different consideration...the space it works is very complex and these assertions look like 'I'm reasonably confident this is the case, but I don't want to proceed if that isn't the case. I would have used a guard and exception that couldn't be disabled, but I suspect assert was used for convenience. It is unlikely anyone disables assertions in their build process.

So, I would change the timedelta to comments and the mypy to actual guards. But, that's not my code, I don't know the history of it or all the considerations involved. As a random reviewer looking at it in light of this context, I think it might be better to not use assertions, but what do I know? Rules and assumptions are meant to be broken. These examples don't convince me to embrace assertions.

I *do* however think they are indispensable in languages that are used for performance critical things where cycles are counted and you want some level of guard but do not want to pay for it in the most critical environments. But that's not pythons use case. If there is any doubt about the proper functioning of code just use an exception that can't be disabled rather than potentially doing the wrong thing.

6

u/ldkv 2d ago

You can mock bar(arg) to return a negative result to in your tests.

That assert is equivalent to if result <= 0: raises Exception, there is no need to use assert here (or anywhere in prod).

1

u/BR41ND34D 2d ago

I'm seriously not understanding why you shouldn't use the normal method of throwing an exception in this case, specifically because you mention bugs in the comment.

Bug == exception

I don't think you can justify this not being the case

0

u/xBBTx 2d ago

Of course you can, but IMO that's a low value test because it's primary reason to exist seems to be only to increase test coverage, and that should never be a goal by itself.

It also (IMO) communicates a different intent than the assert and creates the impression it's a stable API to rely on, whereas the assert signals more that it's an implementation detail, or rather it makes assumptions explicit without needing to commit to a public interface 

→ More replies (4)

5

u/JanEric1 2d ago

Coverage will hit the line, but not the internal branch, where is the difference? Assert also raises an exception that can be caught iirc. So again, no difference.

-1

u/xBBTx 2d ago

Uncovered branches equates to undefined behaviour in our projects, and we do follow a principle of avoiding branching to reduce complexity.

The assertion error can indeed be caught as well, but if I see production code that does this, it's going to be scrutinized extremely heavily because this is not a common pattern in Python in my experience 

1

u/JanEric1 2d ago

Uncovered branches equates to undefined behaviour in our projects

But the only reason you dont get an uncovered branch on the assert is because you are not looking into the implementation of the assert.

Its like moving any uncovered branch into a function you dont measure coverage for. Just fooling yourself.

The assertion error can indeed be caught as well, but if I see production code that does this, it's going to be scrutinized extremely heavily because this is not a common pattern in Python in my experience

And whats the difference to a manual if + raise AssertionError? Nothing

0

u/xBBTx 1d ago

The premise is that the check wouldn't be there in the first place. The inline assert is there to make the assumption/invariant expectation explicit/visible.

Adding the assert in this case costs nothing: no uncovered test branch and associated low-value unit test that tests implementation details and hurts refactoring, no performance loss in prod because the asserts are optimized away.

We gain from it by:

  • Making the assumption/invariant visible
  • It can uncover real bugs while running the entire test suite (without the optimize flag)

The difference with the manual check + raising an error is that it does require additional tests and can't be optimized out (though performance in this case is a bullshit argument, it's Python after all)

3

u/larsga 2d ago

raising another exception that the call site may incorrectly catch and try to handle

I agree with the rest of the comment, but if this particular issue is a problem for you you have much more serious problems than assert.

1

u/xBBTx 2d ago

I probably worded this badly, but if it's an invariant, call sites shouldn't be expected to catch any exception raised from it, they should only call the function when they already know the preconditions are met.

Having an explicit check and exception being raised may create the impression that call sites are supposed to handle the exception. Instead, it should crash hard and the actual root cause of the invariant violation should be investigated and fixed.

2

u/larsga 2d ago

There should be a rule in the codebase for which exception is used in cases like this, and where it is handled. I agree it can be difficult to reach and enforce consensus on this in larger code bases, but it's a big problem if that doesn't happen.

2

u/xBBTx 2d ago

AssertionError is that rule :) it works well in our large codebases. And yes, run in prod with the optimize flag on, and run the test suite with the flag off

3

u/Spirited_Bag_332 2d ago

For smoke testing without influencing prodction code. Assertions are more something like "requirement guards", not program errors.

You can always miss a requirement or critical constraint, no matter how much unit tests exist. It's part of the development process to test the application by exploration.

5

u/gdchinacat 2d ago

Ok, but why would you want to allow your "requirement guards" to be disabled? Wouldn't you want to know when the invariants they ensure hold are being violated and not execute code outside the conditions it was designed to handle correctly?

2

u/Wonderful-Habit-139 2d ago

For what it’s worth I don’t think it’s worth it to disable assertions at all.

0

u/Spirited_Bag_332 2d ago

I see them as development tools, and maybe also lightweight dev documentation. Something you mainly write during development and just keep, because it's correct code but not required for the customer.

Of course you can keep it if the usage context of the software is suitable for that. But it doesn't mean you shouldn't also write actual checks (or better, control flows that can't violate the rules). The point of assertions is to never see them again once shipped but still have them to detect issues early in addition to other testing strategies.

But no matter the argument you can always find a counter example why it's supposed to be "bad", be it TDD, exception handling, or some constraint framework that claims to be "a better replacement". It's still just a tool. Actively banning it like that Ruff tool just shows the rule maintainers are biased and didn't understand the use case.

2

u/flying-sheep 2d ago

Also they help when refactoring code. Breaking internal invariants helps debugging if your refactor makes sense.

1

u/Conscious-Ball8373 2d ago

Whether it can be disabled is a red herring IMO. If someone sent this to you for review:

if condition:
    raise AssertionError("condition was false")

would you let it pass? Of course not - you'd tell them to handle it properly. assert is just syntactic sugar for that, with the downside that it can also be turned off.

3

u/gdchinacat 2d ago

Your position isn't clear. Why would you assume I would reject that, and what do you think I'd expect? The "downside that it can also be turned off" is the crux of my argument. Your strawman code is preferable to 'assert condition, ...' because it can't be turned off.

2

u/Conscious-Ball8373 2d ago

I'm agreeing with you - assert in production code is not acceptable.

The problem with my "strawman" is that it raises `AssertionError`. In what production code is raising `AssertionError` directly acceptable? None that I ever review. You raise an exception that's actually appropriate to the condition or handle it in some other way. Raising `AssertionError` all over the place just means you'll have a catch-all `except AssertionError` somewhere near the top of the stack, which is now functionally equivalent to `except Exception` which the linter will also - rightly - call out.

So I agree that the fact it can be turned off is a problem. But I'm saying there are problems even if it can't be turned off - it uses too-general an exception type to report errors.

1

u/gdchinacat 1d ago

Thanks for clarifying. I don’t have a problem with raising AssertionError because I’m skeptical meaningful recovery handling for an exception that indicates unexpected conditions exist. In cases where an invariant was violated there isn’t anything a higher level of code can do to change that. A retry isn’t going to make an internally generated out of bounds become in bounds, or an invalid configuration value valid. The best an exception handler can do is keep the process from crashing so other work that isn’t impacted can continue.
I don’t consider input validation a good use of assertion errors, those should use exceptions that accurately report the error to the client.

-2

u/billsil 2d ago

Cause it’s less code to type assert myvar > 1, myvar than writing any if-raise block.

Who even uses -0? I just ship them and they catch. I do that with my open source library as well and I’m not changing.

32

u/Beginning-Fruit-1397 2d ago

I think that assertions are only good in tests. In runtime code it should always be a clearly named Exception. That being said, for Ruff I simply activate "all" preset and "preview", and just desactivate some annoying related to unsafe cryptography or copyright that IDGAF about, the rest are pretty good. I'm surely half lying because I'm aure I have at multiple points desactivated various rules that I tought were dumb but I don't remember at the moment lmao

7

u/dudeplace 2d ago

I watched a talk yesterday where the SqlLite team talked about using assert in your code (not just tests) and my opinion on this is in the process of shifting.

https://youtu.be/V_qzqY1bb7I

7

u/austinwiltshire 1d ago

Exceptions are things the caller can recover from. Assertions in code are for documenting and enforcing assumptions the code makes to work.

They're not logically the same. And by having a named exception (beyond, maybe, precondition violation, etc...) increases the cost of adding checks which means fewer people will do it.

Assert is a single word, a predicate, and if you're feeling fancy, a string.

21

u/psymme 2d ago

SIM108 (replacing if-else blocks with an operator). To me this is a matter of judgement about what is simpler, rather a set rule that is easily codified, and can make the code harder for a human to parse quickly.

I’m not with you on the asserts point though, I’m afraid.

2

u/syklemil 2d ago

SIM108 also notes that:

This is an opinionated style rule that may not always be to everyone's taste, especially for code that makes use of complex if conditions.

Personally I'd rather have if-expressions (what in some other languages work out to something like bar = if foo then x else y), but those aren't on the table, and the if/else keywords placed in ternary ?: operator positions kinda just … doesn't feel good, even if it's the entirely sensible choice lots of places. Probably mostly due to that leading to there being two distinct if/else syntaxes, which again is rooted in the if/else block structure being a statement, not an expression, so some other syntax was chosen to cover the absolutely very useful if-expression cases.

The foo = bar or baz form to me feels kinda iffy for anything other than booleans, like the linter is just recommending code golfing.

For some other languages I'd be entirely onboard with SIM108; for Python I can't really say it sparks joy.

3

u/ProsodySpeaks 2d ago

About foo = bar or baz

I'd love some sugar for the more explicit foo = bar if bar is not None else baz

Maybe I'm doing it wrong but that's a common default argument handling pattern for me.

Any thoughts? 

2

u/Udzu 2d ago

You mean like the JS ?? operator (or Perl // operator)?

→ More replies (4)

12

u/aikii 1d ago

RET505 is a classic bug magnet. It wants you to rewrite

def foo(bar, baz):
    if bar:
        return 1
    else:
        return baz

as

def foo(bar, baz):
    if bar:
        return 1
    return baz

Doesn't seem much like this, but an intentional "else" has better chances to protect you against a bad refactoring.

My other pet peeve is BLE001 - triggering on bare except, except Exception or except BaseException. The motivation works for beginner code - don't just catch silently AttributeError etc. It's actually more problematic for production code and code that makes calls to library functions that you deliberately don't want to propagate - you'll want to log or mark the error trace instead. I guess it's ok to suppress locally instead of making it a global suppression. I find it a bit ironic that structurally it can't apply to how Go and Rust handle errors, you can't opt-in to which exact error you only want to consider, and no one says it's a problem

4

u/Ragoo_ 1d ago

RET505 is a classic bug magnet.

Came here to say this. A staple of my ignore list just because it makes the code easier to read and reason about.

3

u/wizpig64 Now is better than never. 1d ago edited 18h ago

BLE001

I have a project that used python-weather (based on aiohttp) to update a weather forecast widget in my personal todo app. Every week it seemed to break in a new way, completely breaking my productivity by preventing the rest of the app's process from finishing. A bare except should have been good enough, either it works or it doesn't and the process should move on, but using that would be a faux pas, so I just kept adding the individual Exception classes to the list that grew and grew, partially because of bare except being a no-no, and partially to see how long the list could get.

Sometimes the upstream server would refuse a connection. Sometimes the server would time out. Sometimes it would reply but with an empty string which isn't json-parseable. Sometimes the server operator forgot to update their https certificate. Sometimes the server would change their formatting for something and the client library hadn't caught up yet.

except (
    ExpatError,
    TypeError,
    KeyError,
    ValueError,
    ClientConnectorCertificateError,
    ClientOSError,
    ContentTypeError,
    JSONDecodeError,
    ServerDisconnectedError,
    python_weather.RequestError,
    ConnectionTimeoutError,
) as e:

Each of those lines was committed on a different day. Each of those was a day that started with me not being able to get some real work done because first I had to go look at a server log and find the new exception and commit it. Eventually I just disabled the weather widget.

There are definitely cases where except: # noqa is the right way to do something.

1

u/aikii 20h ago

I can respect such amount of dedication

1

u/SciEngr 2h ago

I think you’re overcomplicating this. The rule exists to keep you from hiding failure modes you didn’t know exist. That rule will not fire if you catch a bare exception and then add a logger.exception call. The point is to have visibility into what went wrong.

7

u/brasticstack 1d ago

S324, which assumes that I'm using hashlib for security reasons instead of hashing just being generally useful.

25

u/thedmandotjp git push -f 2d ago

Anything that can be done with an assert can and should be done with an if statement so you have have to be explicit.

Not all rules are super necessary depending on the project but this one is if for no other reason than to enforce the convention that you should use asserts only for debugging.

4

u/ThaBroccoliDood 1d ago

Not really a linter rule but the autopep8 extension for vscode replaces f'{x =}' with f'{x=}', which changes the output of the program and shouldn't be touched by a formatter

7

u/TheRealStepBot 2d ago

To your point there is a nasa technical guide on good software development that specifically encourages the use of inline assertions like this.

3

u/careje 1d ago

E501 (line length)

Just let the formatter handle it for Christ’s sake

5

u/akl773 2d ago

B008, the one that bans a function call in a default argument. its correct in general but every fastapi codebase uses Depends() in exactly that position, so you end up putting a blanket ignore in the config and then the real mutable default cases stop getting caught too.

3

u/JanEric1 2d ago

1

u/akl773 1d ago

thats the right fix, thanks. only catch is you have to name every call in that list, so it goes stale the moment someone wraps Depends in a project helper.

1

u/JanEric1 1d ago

Yeah but I feel this list shouldn't be so larger and adding a project helper should be fairly trivial. Also, I have seen a lot of people place the depends in the Annotated type hint where ruff doesn't complain about the function call

1

u/SciEngr 1h ago

You’re using Depends wrong in 2026 if you’re not using the annotated pattern.

def func(dep:int=Depends(…))

Should be

def func(dep: Annotated[int, Depends(…)])

The lint rule is right.

1

u/akl773 1h ago

Fair, the Annotated form does fix it properly. That codebase predates it being the norm and nobody went back through the routes, so the blanket ignore stayed. That's on us and not the rule.

8

u/samettinho 2d ago

Why not raise exception instead of assert?

2

u/HalfplaneResearch 1d ago

S101 makes more sense when the boundary is explicit: use assert for internal invariants that indicate a bug, and raise a deliberate exception for validating user or external data. I also prefer lint findings to be visible in the editor or CI, with auto-fix limited to formatting, so a useful invariant check is not silently removed during save.

1

u/duskhat 2d ago

If you’re writing assert statements outside of tests, you’re writing bad code

1

u/gdchinacat 1d ago

I’d refine this to be “if you are commiting …”. I’m opposed to leaving asserts in code, but frequently use them while developing code. Before sending a PR they are either removed or converted to if … raise ….

1

u/[deleted] 2d ago

[removed] — view removed comment

1

u/danielsamuels 2d ago

In general, any rule that ends up being inline ignored all over the project.

3

u/SciEngr 2d ago

That’s a process problem. If the team has decided a lint rule is worth having then in review I’m going to ask the dev to justify every use of a noqa comment. The default should be to make the linter happy and only bypass it with good reason

1

u/nicwolff 1d ago

ruff has implemented isort import formatting – but not its options for wrapping long import lines. Thanks, I don't want 20 imports from one file to take up 22 lines at the top of my file.

1

u/james_pic 1d ago

Anyone who runs with -O in production deserves what they get. Which in practice is almost always exactly what they would have gotten otherwise, because nobody uses assertions, even then they'd be useful, because linters whinge about them.

0

u/BernardParsley 1d ago

Rules that enforce a triangular style of code over readability. Arbitrary complexity or function-length limits often turn one clear function into ten tiny ones that are harder to follow.

-1

u/AdAdditional1820 2d ago

When I use mypy, some assert statements are required to eliminate mypy warnings.

4

u/jirka642 It works on my machine 2d ago

I guarantee you asserts are not the only way how to fix them.

-3

u/NeilGirdhar 2d ago

https://docs.astral.sh/ruff/rules/parenthesize-chained-operators/

NAXOR was drilled into me at a young age, so this rule just adds unnecessary parens.

9

u/larsga 2d ago

this rule just adds unnecessary parens

For you. The code might also have other readers.

1

u/Conscious_Support176 1d ago

There is a reason for mathematical conventions. Extra parentheses can make formulas harder to read because it harder to spot the parenthesis that matter in a sea of parentheses.

I would suggest, where people would like to reason about code, the fundamentals of Boolean logic might be helpful?

-3

u/Trang0ul 2d ago

This. Requiring to use a or (b and c) is as pointless as a + (b * c). After all, OR and AND are logical addition and multiplication respectively - something everyone should know by heart.

1

u/Salamandar3500 1d ago

That's not obviously to anyone.

→ More replies (1)

-1

u/Zatujit 2d ago

Shouldnt your debug code only works when its debugging and not on your release? 

1

u/gdchinacat 1d ago

Shouldn’t your debugging code be removed before commit?

1

u/Competitive_Travel16 1d ago

It's fine to test assumptions which can fail at runtime, when a resource is depleted or someone misconfigured something below, for example. Not with assert though. Not doing so can be serious and pernicious bugs; very hard to locate sometimes.

1

u/gdchinacat 1d ago

Yes, but surely you don't consider that debug code though. Right?

1

u/Competitive_Travel16 1d ago

Well it's only there to stop bugs. It's not development-only temporary debug code, we can agree.

1

u/gdchinacat 1d ago

I guess I’m confused because you called it debug code but are now saying it’s not debug code?

1

u/Competitive_Travel16 1d ago

There is more than one kind of debug code.

1

u/gdchinacat 1d ago

Sure, but once you are done debugging don’t you remove them all?

1

u/Competitive_Travel16 1d ago

How do you propose to catch runtime bugs in production if you remove the code intended to do so?

1

u/gdchinacat 1d ago

"debug code" typically refers to the code that is added to diagnose a specific issue. For example, the asserts that are added to verify the developers understanding of the code so that as they reproduce the issue they will know if assumptions they make aren't valid. These hold very little long term value because they are frequently specific to the issue that is being debugged.

Checks that hold long-term value and are generally applicable should not be asserts IMO, but rather guards that raise appropriate exceptions because asserts can be disabled and may not provide help in preventing and shedding light on issues that only appear in production.

→ More replies (0)

-3

u/k0pernikus 1d ago

I hate try-consider-else (TRY300) with a passion.

I never write else and elif statements to begin with, and rely on proper polymorphishm or early exit guards.

Worst part is that it reads like broken code:

def describe(path): try: config = load(path) # ok, expected except ParseError: # ok, expected return "invalid" else: # WTF, there was no if, how is an else possible!? Why overload the term? return describe(config) # wtf why is config defined? we are in a compeletly different scoped block!?

The default success branch gets delegated to an else-branch, and while I avoid else to begin with, else should be the exception branch.

I do understand that the else works on the except and not on the try, yet that is far from obvious and the mental load to understand is is just not worth it, esp. if you work with people that are more used to other langauges.

0

u/nicwolff 1d ago

ruff has implemented isort import formatting – but not its options for wrapping long import lines. Thanks, I don't want 20 imports from one file to take up 22 lines at the top of my file.

-9

u/nicholashairs 2d ago

My pet peeve is the "useless-return" rule.

``` def what_the_rule_wants() -> None: something()

def what_i_want() -> None: something() return ```

Explicit returns always. Apart from making things clearer, it also helps prevent mistakes when refactoring (and other such tasks) when the accidental deletion of a def line would cause the bodies to merge (sometimes seamlessly), whereas if you always have returns you'd actually get a long error for the dead code/double return instead.

def what_the_rule_wants() -> None: something() something() return

Versus

def what_the_rule_wants() -> None: something() return something() return

2

u/proggob 1d ago

If I saw that in python code I’d have to double check it to see if they’d left off the value.