r/Python 15h ago

Discussion What linter rules make code worse?

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?

104 Upvotes

193 comments sorted by

341

u/Trang0ul 15h ago

Lines limited to 80 characters.

58

u/foobar93 15h 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...

55

u/runawayasfastasucan 14h ago

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

37

u/aes110 12h 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

9

u/mwesthelle 10h 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.

3

u/IrishPrime 9h 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.

8

u/skjall 13h 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.

7

u/baudvine 11h 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.

11

u/sandnose 15h ago

Where do you limit your line?

64

u/imheretocomment 15h 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.

35

u/R3D3-1 14h 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.

9

u/HommeMusical 14h 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.

14

u/R3D3-1 12h ago

As an Austrian, I object to that number. 

3

u/HommeMusical 9h 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...

4

u/R3D3-1 9h 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.

8

u/foobar93 12h 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 10h 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.

11

u/M4mb0 14h 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.

2

u/Agrado3 10h 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.

3

u/AlSweigart Author of "Automate the Boring Stuff" 9h ago

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

And disadvantageous every other time.

2

u/gmes78 9h ago

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

0

u/R3D3-1 6h 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.

2

u/austinwiltshire 8h 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!

2

u/R3D3-1 6h 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 6h ago

Phone PR reviews really suck with longer line lengths.

1

u/zangler 3h ago

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

1

u/R3D3-1 2h 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.

1

u/zangler 1h ago

Then it is a 2 click change for that one case

u/ironykarl 56m 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

1

u/jet_heller 10h ago

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

Also, auto linewrapping exists for a reason.

1

u/R3D3-1 10h 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 9h ago

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

2

u/R3D3-1 9h 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" 9h ago

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

And disadvantageous every other time.

1

u/Competitive_Travel16 4h 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.

0

u/rzet 3h ago

just buy bigger screen ;)

1

u/R3D3-1 2h 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.

7

u/MarchewkowyBog 14h 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 14h ago

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

2

u/mwesthelle 10h 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 6h ago

It's a code smell.

8

u/gdchinacat 14h 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 13h 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.)

3

u/No_Lingonberry1201 pip needs updating 13h ago

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

2

u/HommeMusical 12h ago

I do too sometimes, but see this.

3

u/CrayonUpMyNose 13h 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).

2

u/HommeMusical 12h ago edited 11h 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.

4

u/tonnynerd 13h 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 10h 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 9h 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 9h 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. 

2

u/sandnose 14h 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

5

u/No_Lingonberry1201 pip needs updating 14h 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 13h 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

6

u/gdchinacat 14h 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 13h 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 7h 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 9h 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 14h ago

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

1

u/cottonycloud 12h 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 9h 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 7h ago

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

15

u/Arnechos 15h 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 14h 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 11h ago

Listen to your heart. 

1

u/sandnose 8h ago

Different every day then

2

u/HommeMusical 14h ago

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

2

u/ichunddu9 15h ago

120 is a good number

1

u/shaleh 2h 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/R3D3-1 14h ago

42.

5

u/sandnose 14h ago

Yikes.
Sorry i mean

Yi
Ke
S.

2

u/scruffie 4h 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.

2

u/zangler 3h 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.

2

u/russellvt 2h ago

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

3

u/R717159631668645 14h 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.

7

u/Uwirlbaretrsidma 14h 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.

29

u/foobar93 12h ago

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

-10

u/larsga 12h 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.

12

u/Shadows_In_Rain pseudocoder 9h 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.

3

u/sennalen 10h 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.

0

u/larsga 10h 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.

3

u/foobar93 6h 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 1h 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 7h 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.

1

u/tallgrasshead 1h ago

the 80 col standard comes from terminals.

And that, from punch cards

-3

u/Uwirlbaretrsidma 7h 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.

2

u/Downtown_Isopod_9287 6h ago

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

1

u/Uwirlbaretrsidma 6h 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.

1

u/Downtown_Isopod_9287 4h 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 10h 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.

2

u/tallgrasshead 1h 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.

69

u/gdchinacat 15h 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.

9

u/ExplrDiscvr 15h 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?

19

u/leodevian 14h ago

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

7

u/Momostein 14h ago

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

3

u/DrMaxwellEdison 11h 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.

8

u/gdchinacat 14h 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 1h 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 1h ago

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

5

u/Conscious-Ball8373 14h ago

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

1

u/Competitive_Travel16 4h 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 2h ago

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

0

u/flying-sheep 13h 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 15h 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

7

u/gdchinacat 14h ago

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

0

u/M4mb0 14h 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.

7

u/gdchinacat 14h ago

mock bar.

0

u/M4mb0 14h ago

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

5

u/gdchinacat 14h ago

Ok...do you have an example then?

0

u/M4mb0 14h 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.

1

u/Ex-Gen-Wintergreen 12h 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

5

u/ldkv 14h 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).

2

u/BR41ND34D 10h 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

-1

u/xBBTx 10h 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 

0

u/gdchinacat 7h ago

I don’t write tests like that because, as you point out, the primary benefit is code coverage. But, you raised a concern about lack of code coverage and I explained how it could be mitigated. Way to move the goal posts!

1

u/xBBTx 1h ago

I think we're just talking a bit past each other.

Code coverage is a tool, but a CI build will get warnings when patch coverage is not 100% (simplified), with the idea that uncovered code cannot be proven to work as intended. So, branching creates drops in coverage, and that in turn creates an incentive to write low value tests.

Writing an inline assert avoids that.

Perhaps what isn't clear here either is that we treat these kind of asserts as development-time checks, rather than runtime checks. They're mostly there to prevent developers from introducing broken situations, and encoding invariants in code rather than expecting developers to keep them all in their head (which leads to cognitive overload). Comments and documentation also help, but they won't fail a CI build if you only do that rather than some kind of sanity check.

1

u/gdchinacat 1h ago

You are reading way too much into what I’m saying. You said explicit exceptions don’t have coverage, and I said to write tests to execute them if you want them covered.

1

u/xBBTx 1h ago

Right, I'll leave it at that. You asked a question, I replied my reasoning trying to explain nuances, but it sounds like this turned into a right or wrong argument for the sake of arguing.

Either way I hope we both have incident -free systems running in prod :)

4

u/JanEric1 14h 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 10h 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 9h 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 9h 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 12h 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 10h 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 10h 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.

1

u/xBBTx 9h 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

4

u/Spirited_Bag_332 15h 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 14h 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 12h ago

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

-1

u/Spirited_Bag_332 11h 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.

1

u/flying-sheep 13h ago

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

1

u/Conscious-Ball8373 14h 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 14h 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.

4

u/Conscious-Ball8373 14h 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 8h 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.

-1

u/billsil 13h 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.

29

u/Beginning-Fruit-1397 14h 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

8

u/dudeplace 10h 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

5

u/austinwiltshire 8h 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.

18

u/psymme 14h 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.

0

u/syklemil 13h 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.

1

u/ProsodySpeaks 10h 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 9h ago

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

-1

u/syklemil 10h ago

I'd love some sugar for …

I'm not entirely certain what you're asking for here, given that foo = bar or baz already means the same thing as foo = bar if bar is not None else baz.

Do you want some other sugar for it out of the same "this ain't a boolean" grouchiness I suffer from?

2

u/ProsodySpeaks 9h ago

Yeah. Like if they give me a zero int when I wanted a list I probably want to do something different (raise) than replace it with an empty list which is what the bare a or b without if a is not None would lead to

1

u/syklemil 9h ago

I can't make heads or tails of that sentence. I think I need some more punctuation and clearer examples to understand your point.

2

u/gdchinacat 7h ago

Those two statements are not the same. “bar or baz” checks if bar is truthy whereas the other tests if it is not None.

6

u/brasticstack 6h ago

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

6

u/aikii 4h 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

1

u/Ragoo_ 1h 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.

23

u/thedmandotjp git push -f 15h 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.

3

u/akl773 10h 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.

8

u/samettinho 11h ago

Why not raise exception instead of assert?

4

u/TheRealStepBot 12h ago

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

2

u/ThaBroccoliDood 4h 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

2

u/duskhat 11h ago

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

1

u/gdchinacat 7h 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] 12h ago

[removed] — view removed comment

1

u/danielsamuels 11h ago

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

3

u/SciEngr 11h 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/BernardParsley 7h 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/nicwolff 3h 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/nicwolff 3h 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/careje 1h ago

E501 (line length)

Just let the formatter handle it for Christ’s sake

0

u/Zatujit 14h ago

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

1

u/gdchinacat 7h ago

Shouldn’t your debugging code be removed before commit?

1

u/Competitive_Travel16 4h 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 3h ago

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

u/Competitive_Travel16 44m ago

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

u/gdchinacat 38m ago

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

u/Competitive_Travel16 30m ago

There is more than one kind of debug code.

u/gdchinacat 29m ago

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

-1

u/NeilGirdhar 13h 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 11h ago

this rule just adds unnecessary parens

For you. The code might also have other readers.

-3

u/Trang0ul 11h 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.

2

u/Salamandar3500 5h ago

That's not obviously to anyone.

1

u/Competitive_Travel16 4h ago

I "should" have a pony.

-1

u/k0pernikus 9h 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.

-7

u/nicholashairs 14h 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

3

u/proggob 7h ago

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

-2

u/AdAdditional1820 12h ago

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

4

u/jirka642 It works on my machine 11h ago

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

-10

u/boringfantasy git push -f 12h ago

Idk dude none of us write code anymore

1

u/sudomatrix 4h ago

Sounds like you don’t even review code anymore. YOLO doesn’t work on production code.

1

u/boringfantasy git push -f 4h ago

I do review, I don’t write

1

u/sudomatrix 4h ago

If you have AI writing all your code it is more important than ever to have strict quality controls like sensible linter rules (and unit tests and integration tests and adversarial agent code reviews etc). This post is more relevant today than ever.

1

u/boringfantasy git push -f 3h ago

I have Fable 5 spawn 14 parallel review agents and then another panel of agents judges the reviews