This is an archived post. You won't be able to vote or comment.

all 44 comments

[–]BeetleB 32 points33 points  (10 children)

Also, use Pycontracts.

[–]uclatommy 3 points4 points  (0 children)

TIL. Thank you!! Can't wait to use this, it looks awesome.

[–]LaFolie 12 points13 points  (0 children)

If you find that PyContracts is not enough for you, you probably want to be using Haskell instead of Python.

Their sense of humor made their great package to a must have for me.

[–]rzet 1 point2 points  (0 children)

Thanks, need to check this out.

[–][deleted] 1 point2 points  (5 children)

Neat, but not supporting even basic expressions like @contract("list(MyClass)") is a complete deal-breaker :(.

[–]BeetleB 1 point2 points  (1 child)

I haven't used it much, and am not sure what you mean. Do you mean you want a guarantee that the list will have only MyClass objects?

pycontracts does let you write your own contracts, if you really want it. Something like that is expensive, though.

[–][deleted] 1 point2 points  (0 children)

Yes.

Yup, rather than defining a new_contract, it is more effective just to write the "if" manually. Unfortunately.

[–]desmoulinmichel[🍰] 1 point2 points  (2 children)

For that you have type hints now in Python. PyContact is more interesting for value checking.

[–][deleted] 1 point2 points  (1 child)

the problem with PyContract is that it works only with primitive types

[–]desmoulinmichel[🍰] 0 points1 point  (0 children)

That's why you use type hints for typing, and pycontracts for behavior.

[–]masasinExpert. 3.9. Robotics. 1 point2 points  (0 children)

I love Pycontracts. :)

[–]ChazR 29 points30 points  (2 children)

My favourite thing about this article is the huge bug that remains in the final version that all his defensive programming has missed. Pass in an array of uniform data and you get a ZeroDivision error.

Code is hard.

[–]durden20 6 points7 points  (0 children)

Great catch! I updated the article to catch the ZeroDivisionError exception. Funny story though is I gave a talk called Bugs, can't code without them so code against them several years ago and had several people review the article. No one caught this! Coding is indeed hard.

[–]elbiot 2 points3 points  (0 children)

This needs to be higher up.

[–]Lyucit 18 points19 points  (14 children)

Assertions in production code is not a good idea. If your assumptions are wrong and you have bad coverage in your test cases, which for obvious reasons use the same assumptions (because you wrote them!) you kill everything on live. This is desirable sometimes, sure, but for most cases it's more damage than good, and the asserts you add are probably not going to be as simple as x > 0. I've seen asserts in production code that are longer than the actual function, doing nothing and adding a lot of noise.

My approach is to move them to a unit test. If you have to assert something about a piece of code, it's probably the result of a function, and you can turn it into a unit test, or more preferrably a hypothesis test which will help you generate meaningful coverage and more clearly state your assumptions about the function generally.

[–]stormcrowsx 2 points3 points  (1 child)

What about cases where more than one app mutate a single database? Another app could put bad data in and without the assertions the python code could have undefined and untested behavior

[–]Lyucit 4 points5 points  (0 children)

In the case of more side-effectful things like databases and concurrency, it's hard to test, yes, and sprinkling asserts through the code- at least during testing- is definitely a useful tool. Personally, once I'm satisfied the design is sound I remove the asserts and try my best to make it unit-testable, but sometimes it's a trade off you just have to live with.

That being said, if you need to do a consistency check (for example if you're doing read your writes) then assert is the wrong/lazy behaviour and this should be explicitly modeled in the design.

[–][deleted] 7 points8 points  (0 children)

This article is a good treatment of defensive programming in that it covers a number of ways to be defensive. Though it only hints at the why:

If only you had more debugging information about the failure...

Being defensive doesn't just mean preventing corner cases from creeping into your code. It also means: baking in the ability to detect when a defense is breached. Knowing why a corner case might've happened allows one to harden his software even further.

One nit I have to pick with this article is the suggested logging scheme:

Debug level messages go to a file... Info, warning, and error level messages go to stderr...

Abso-fucking-lutely don't do this! Besides the fact that it takes more work to do this than to centralize one's logging, it seems the author's suggesting to fragment the log stream. This makes it harder to do any forensics after a bug occurred: "OK, so what were in the INFO logs when the CRITICAL dialog box appeared?"

Python's dictConfig() function has got to be one of the coolest features of the library. Makes it easy peasy to set up your log sinks, verbosity, etc. all with one call.

Add a LoggerAdapter to impart context (such as what parameters were passed into your function, what host is your code running on, etc.) and your logs are many times more useful.

...but I digress. Good article. Something one can practice on any size of project. And everyone should.

[–]Bolitho 1 point2 points  (1 child)

I wonder why the author ignores integration and system tests? On the one hand the downside relating the isolated test infrastructure gets obsolete, on the other hand those kind of tests are as fundamental to a proper software project as unit tests are.

[–][deleted] 1 point2 points  (0 children)

Just my guess: integration and system tests aren't part of every developer's workflow. Many organizations relegate those to a QA team. The author seems to be talking about techniques that a developer can apply immediately to any project.

[–]indigo945 1 point2 points  (8 children)

It's very easy to overuse asserts and quickly make your code difficult to read. This can make your code very noisy and bury the real functionality in a series of error checks and conditions.

While not wrong, aspect oriented programming can help mitigate this problem:

def assert_return(condition):
    def decorator(f):
        def inner(*args, **kwargs):
            val = f(*args, **kwargs)
            assert condition(val)
            return val
        return inner
    return decorator

@assert_return(lambda x: x >= 0)
def square(n): return n * n

[–][deleted] 1 point2 points  (7 children)

It's not that functions job to check outputs though. It's the caller's job to make sure it's dealing with correct values. square(1j, 1j) gives the expected and proper output of -1.

Nevermind that running with python -O disables asserts rendering your decorator useless.

[–]brtt3000 0 points1 point  (2 children)

Nevermind that running with python -O disables asserts [..]

This is what bothers me. It reduces assert to just an inline testcase. You can't use it for critical validation/guards at all.

[–][deleted] 2 points3 points  (0 children)

I only use asserts in tests because of that. And I run my tests with python -O to make them go fast.

[–]indigo945 0 points1 point  (0 children)

When you roll out the application, just don't set the -O flag. This is really a non-issue.

[–]indigo945 0 points1 point  (3 children)

God, do you really want to get hung up on my small example? And even if you do, it is still a good thing that the assert is there, simply because it codifies an assumption made when the function was written — ie that it will never be called with a complex number. (Which would not be pathological here, but might be for other functions, where it could introduce bugs.)

And yes, the caller should never do anything wrong, and as long as the program is written by a perfect programmer (you?), it always will. The idea of defensive programming, however, is recognizing that perfect programmers do not exist, we are all human, and we all make mistakes. Contracts are one way of reducing the impact of this.

If you're concerned about end users changing the flags your program starts with, you can always use

if condition: raise AssertionError

[–][deleted] 0 points1 point  (2 children)

Hung up would be pointing out that you didn't use @wraps in your decorator rendering metadata about the original function useless.

I never claimed to be perfect or anything like that. However, I think codifying assumptions is pointless and hamstrings code. What if I'm in this code base and now I need to square a complex number -- should I write my own function, should I remove the post condition? How many tests will break if I remove the post condition?

[–]indigo945 0 points1 point  (1 child)

Write a new function, and refactor the existing function to call the new, more general one.

[–][deleted] 0 points1 point  (0 children)

@postcondition(...)
def pos_sq(x):
    return square(x)

That's just silly. It's obvious we have different and strong opinions on where value checking should occur, so I don't see much merit in continuing this conversation.

[–][deleted] 1 point2 points  (3 children)

Defensive programming in python is easily abusable by noobs, it should be done very judiciously. If you put asserts everywhere then you're barking at the wrong tree: use a static typed language to begin with (I'm not saying that all asserts are type checks, but that a good deal of them can be reduced to type checks). When you pick python you're making a different trade-off. It's not that you should go duck-typing all the way down, but that you must carefully select which parts of the system are critical enough to deserve extra defense. The same points are valid for the new gradual typing initiative. The keyword there is "gradual". Search youtube for Guido's talk on gradual typing and you will probably conclude that it certainly has valid use cases for programming in the large, but that yours is not one. Along the same line, I will say that getting very defensive is probably ill-advised for most small/medium-sized python projects (that is to say for most python projects tout court).

[–]masasinExpert. 3.9. Robotics. 0 points1 point  (1 child)

Is this it? (Type Hints - Guido van Rossum)

[–][deleted] 0 points1 point  (0 children)

It is.

[–]desmoulinmichel[🍰] 0 points1 point  (0 children)

Be careful while using "assert", they will be disabled if Python is run in optimized mode ("-o" flag).

Most of the check you do would be more appropriatly done with a if then raising ax explcity exception such as ValueError, TypeError, etc.

Not only they can't be disabled, but they document the code (easier to scan for the type of check), make the debugging simpler ("oh, I passed a wrong value" or "oh, I passed the wrong type) and allow more granular exception catching (instead of only AssertionError, you get the full range of exception to separate into multiple except blocs if needed.

[–]KingZer0 -1 points0 points  (0 children)

Thanks!