Writing/The Unbearable Lightness of Coding

Chapter 3 · 26 min read

Conditions: If, Else and Boolean Fluency

Next, let's talk about conditions.

And although it might sounds basic — like, what is there to say about if-statements? — hopefully the previous chapter has demonstrated that the answer is "surprisingly, a lot".

We'll talk about if and else, of course, but also about boolean fluency — what makes a good condition, how short circuitry works, the evolution of the ternary operator and more.

But first things first: the syntax. Suppose we have:

>>> x = 1
>>> if x > 0:
...     print('x is positive')
x is greater than 0

The if keyword precedes some condition, followed by a then-clause, which happens if the condition is true, and doesn't if the condition is false:

>>> if x < 0:
...     print('x is negative')
# nothing

We can optionally add the else keyword, followed by an else-clause, which only happens if the condition is false:

>>> if x > 1:
...     print('x is greater than one')
... else:
...     print('x is less than or equal to one')
x is less than or equal to one

There's also elif, a shorthand for else-if, which we can add in the middle to chain a few cases together. Compare a situation where the if-statements aren't connected, so both clauses can happen if both conditions are true:

>>> x = 2
>>> if x > 0:
...     print('x is greater than zero')
>>> if x > 1:
...     print('x is greater than one')
x is greater than zero
x is greater than one

To a situation where they're coupled with elif, so only the first case to hold true actually happens. The rest are discarded and skipped, even if they would hold true as well:

>>> if x > 0:
...     print('x is greater than zero')
... elif x > 1:
...     print('x is greater than one')
x is greater than zero

All of this is great, but what's interesting about conditions is not so much the semantics — it's the rhetorics: how can we make conditions more coherent, more concise, and otherwise more eloquent.

Truth Values

Let's start with the fact that in Python, every object has a truth value — it can be evaluated as a condition, or cast to a boolean explicitly with the builtin bool function, in which case it becomes either True or False.

For example, a list is True if it has some elements, and False if it is empty, which has some interesting consequences. Suppose we want to test if a list is empty, so we compare it to another empty list:

>>> items = []
>>> if items == []:
...     print('list is empty')
list is empty

Or maybe we think about emptiness in terms of the number of items, so we compare the list's length to zero:

>>> if len(items) == 0:
...     print('list is empty')
list is empty

Both methods work, and are pretty clear — but also, they're needlessly verbose, and invoke needlessly complicated behavior, whether it's allocating an empty list or calculating the length.

Admittedly, both are pretty fast for native lists; but for other data structures they can be decidedly wasteful. Compare them to this:

>>> if not items:
...     print('list is empty')
list is empty

It almost reads like English, and doesn't burden us with new objects or technical details. It simply and directly asks the object, "are you, or are you not?" — and the object answers in a way that should make sense in the context.

That context, by the way, is important, because it's quite easy to overdo.

For example, a number is True if it's non-zero, and False if it's zero; so if we have a countdown, we might be tempted to test when it reaches zero with:

>>> countdown = 0
>>> if not countdown:
...     print('launch!')
launch!

But this sends the wrong message, doesn't it? Compare this to:

>>> if countdown == 0:
...     print('launch!')
launch!

In this context, testing if the countdown has reached zero is pretty spot on.

As you can see, it's not an exact science, which is almost always the case with software design. With that in mind, let's go over a few more "rules" and "best practices" in writing conditions — but stay alert for the nuance.

Positive Conditions

Generally, positive conditions are more intuitive than negative ones. This code, for example:

>>> x = 1
>>> if not x < 0:
...     print('x is non-negative')
x is non-negative

Is better expressed as:

>>> if x >= 0:
...     print('x is non-negative')
x is non-negative

It's just easier to think about x being equal to or greater than 0, than it not being less than 0. In both cases we need to understand the comparison operator; but in the negative case we also need to mentally flip it, effectively arriving at the positive case anyway, but doing so in our head.

Even compound conditions, with ands and ors, can be reorganized so they're less negative. For example, suppose we have a post object with an author and a list of comments, which hold some text and can be hidden:

>>> post = Post('alice', comments=[
...     Comment('comment 1', is_hidden=False),
...     Comment('comment 2', is_hidden=False),
...     Comment('comment 3', is_hidden=True),
... ])

If a user is neither an administrator nor this post's author, they shouldn't be able to edit it:

>>> user = 'bob'
>>> if not (user == 'admin' or user == post.author):
...     print(user, 'cannot edit this post')
bob cannot edit this post

But this negative condition can be reorganized. First, it can be ungrouped, in which case not (X or Y) becomes not X and not Y:

>>> if not user == 'admin' and not user == post.author:
...     print(user, 'cannot edit this post')
bob cannot edit this post

And then not can be compressed into the comparison by inverting it:

>>> if user != 'admin' and user != post.author:
...     print(user, 'cannot edit this post')
bob cannot edit this post

In other words, instead of saying "if the user is not (either an admin or the author)", we're saying "if the user is neither an admin nor the author" — and it's somewhat clearer, isn't it?

Flat Conditions

Another tip you'll often hear about good conditions is that flat is better than nested. Consider a function like this one:

>>> def print_comments(post):
...     if post.comments:
...         for comment in post.comments:
...             if not comment.is_hidden:
...                 print(comment.text)

If a post has comments, it iterates over them, and unless a comment is hidden, it prints it out:

>>> print_comments(post)
comment 1
comment 2

This nesting is stretching our attention, however, and can become difficult to follow, especially in more complicated cases. So we can flatten it, and achieve the same with a few more lines, but with much less nesting:

>>> def print_comments(post):
...     if not post.comments:
...         return
...     for comment in post.comments:
...         if comment.is_hidden:
...             continue
...         print(comment.text)

If the post doesn't have any comments, abort; otherwise — and here we can be sure that the post has comments, and never worry about it again — iterate over them. Again, if a comment is hidden, we bail out — in this case, using continue to skip to the next iteration of the loop — and if it's not, we finally print it out.

This version may be longer than the previous one (or rather, more spread out); but this is only because we're continuously decluttering the context, so that at every moment we can focus on the narrowest possible thing, and not have to care about everything at once.

Chained Conditions

Another trick to condense conditions is chaining. If we're testing that a value is within some range:

>>> x = 0.5
>>> if x > 0 and x < 1:
...     print('x is between zero and one')
x is between zero and one

We can actually do it like so:

>>> if 0 < x < 1:
...     print('x is between zero and one')
x is between zero and one

Not only is this more readable, but it's also more efficient. Consider, for example, what would happen if x was not a name but a function call:

>>> def f():
...     print('calculating...')
...     return 0.5

In the first case, the condition would invoke f twice, and thus have to recalculate its return value:

>>> if f() > 0 and f() < 1:
...     print('result is between zero and one')
calculating...
calculating...
result is between zero and one

In the second case, however, it is only invoked once, and Python is smart enough to reuse the same value for both sides of the condition:

>>> if 0 < f() < 1:
...     print('result is between zero and one')
result is between zero and one

Our First Contradiction

But here, we arrive at a dilemma.

Suppose a list should have at least one item, but not more than a hundred, and we want to print an error if its length is invalid. We can check this explicitly:

>>> x = list(range(100))
>>> if len(x) < 1 or len(x) >= 100:
...     print('invalid size')
invalid size

Or cleverly chain it together:

>>> if not 1 <= len(x) < 100:
...     print('invalid size')
invalid size

This is certainly clearer — if the list size is not within these bounds, it's invalid — and I think it looks nicer; but if you're paying attention, you might say "wait! You've just turned a positive condition into a negative one, which contradicts our first rule!"

And you'd be absolutely right; software design is full of such contradictions. Or rather, those are only contradictions if we mistake all this good advice for "rules".

In reality, good advice is contextual, and there's never a one-size-fits-all solution. Whatever "rules" we develop will inevitably raise the question: when do we follow these rules, and when do we break them?

Lessons from Literature

To gain a better intuition, consider English — like, the language.

If I say, "had I not known, I wouldn't have come," it might take you a moment to decipher. Eventually, you'll figure out that what I mean is that I did come, because I knew something — in other words, "I knew, so I came," which is much clearer, isn't it?

That's what I mean by positive conditions being easier to follow; but it doesn't mean that all negative conditions are incomprehensible. Saying "if the length is less than one, or more than a hundred, then the size is invalid" is about as clear as saying "if the length is not between one and a hundred, then the size is invalid" — in fact, I find the second version to be a bit more cohesive.

Consider another sentence: "Brad Pitt, in the movie Snatch, plays a gypsy who boxes for a living." How would you make it simpler? And what would be the equivalent in code?

For one, "a gypsy who boxes for a living" is needlessly verbose. There's a word for someone who boxes for a living, and it's "boxer"; so we can say instead, "Brad Pitt, in the movie Snatch, plays a gypsy boxer." In code, this reminds me of snippets I sometimes see, where we have:

if condition:
    return True
else:
    return False

It's clear enough, but it's also needlessly verbose. Instead, we can write:

return condition

And this would be enough.

Another problem with our sentence is that it requires us to open a pair of mental parentheses, and for no good reason. "Let's talk about Brad Pitt," it says, "and by the way, we're talking about the movie Snatch. Anyway, Brad Pitt — he plays a gypsy boxer there."

We could say the same thing without zig-zagging, simply by transposting the clauses: "in the movie Snatch, Brad Pitt plays a gypsy boxer". The same is true for code; suppose we have:

if x > 0:
    # complicated code
else:
    return -1

This similarly requires us to keep track of the condition all the way through, just to comprehend the trivial case in which we abort with -1. Transposing the clauses lets us get it out of the way quickly and focus on the rest:

if x <= 0:
    return -1
else:
    # complicated code

In this particular case, we can even get rid of the else entirely, decoupling the conditions and making the code flatter:

if x <= 0:
    return -1
# complicated code

Lessons from Art

What I'm trying to demonstrate is that on one hand, it's pretty hard to nail down precise rules about good programming. On the other hand, once we develop some intuition, like we have with language, it tends to be pretty commonsensical.

To hone this intuition, this common sense, the best advice is to read as much good code as possible, and keep trying to write and rewrite our own code until it's hopefully just as good — pretty much the same advice we'd get for learning how to write well in English.

And just like in languages, there is some value in trying to articulate general patterns, as long as we take them with a grain of salt. If we'd like to improve our English prose, we might read The Elements of Style — and although I find it to be a highly elitist and obnoxious text that takes itself way too seriously, it does have some good points, like the previous Brad Pitt example, or this quote:

It is an old observation that the best writers sometimes disregard the rules of rhetoric. When they do so, however, the reader will usually find in the sentence some compensating merit, attained at the cost of the violation. Unless [the student] is certain of doing as well, he will probably do best to follow the rules.

To illustrate this, I like to give Picasso as an example:

The picture of the left, Science and Charity, he painted when he was 16, which is insane; and the one on the right, The Bull, he drew when he was 64 — but not because he forgot the rules of the game. People that don't like modern art might shrug and say, "well, the first one is better; my five year old can draw that second doodle," and I get that, because not knowing the rules and not following the rules can look similar to a layman.

But true mastery is not just about knowing all the rules. It's about understanding why they're even there; what purpose they serve, and what this purpose presupposes. What's so interesting about Picasso is not that he broke the rules — that, a five year old can do as well — but just how he broke them, and what he meant by breaking them the way he did.

This is the mindset I'd like us to carry over to code, as we edge towards more sophisticated software design: not not to memorize how things are done, but to understand why they are done this way, and to imagine how they can be done differently.

For me, something about the bull makes it one of my absolute favorites; just appreciate how much substance is squeezed into so few lines. It's silly and cute, so it evokes some playful endearment — but not so much as to be infantile. There's some clean dignity about it, too; some aesthetic admiration for the form and function of the bull that is incredibly lucid and sincere. It manages to capture both the real animal and its iconic symbolism in Spanish culture at the same time, doing so in such a compact squiggle that its profundity boggles the mind.

As an aside, it's good advice for any programmer to have (or at least, pretend to have) a favorite Picasso painting. Not only will it help you sound like a sophisticated douchebag, but also serve as a handy excuse for when people ask why your code looks the way it does.

The Strange Case of and/or

Anyway, back to conditions.

Those are made not just of truth values, but also boolean operators between them. not is simple enough — it flips the truth value; but and and or turn out to be a bit more interesting.

For example, what is 1 and 10? You might think it's True — and in some sense, it is — but it's actually more specific:

>>> 1 and 10
10

And what about:

>>> [] and 2
[]
>>> 5 or 8
5
>>> 0 or {}
{}

To understand what's going on, let's ignore the surprising bit — that these expressions don't return True or False, but actual values — and only look at what's happening to the truth values involved, because even the order is confusing; sometimes its the first object, sometimes the second. And to explain this order, we need to talk about short circuitry.

It starts with a small insight: that to evaluate an and-clause, we go over its two (or more) operands, and make sure that all of them are true. So then, the moment even one of them is false (say, the first one), then the fate of the entire expression is sealed; we don't need to evaluate the rest (like, the second).

For or, we go over its two (or more) operands, check whether any of them is true, and when we find one that is, it's enough; there's once again no point in going further.

Using Short Circuitry

This is a nice optimization, which saves us from having to compute values that don't matter to the overall decision. But it also introduces a certain guarantee into a condition: that later things will only happen if what preceded them explicitly did (in case of and) or didn't (in case of or).

So if we want to check if some string is equal to "yes", ignoring its case, this function will do:

>>> def is_yes(s):
...     return s.lower() == 'yes'

>>> is_yes('YES')
True

That is, until we pass in a number and get an error:

>>> is_yes(1)
AttributeError: 'int' object has no attribute 'lower'

If we decide to add a type check to our function, it won't help to stick it at the end:

>>> def is_yes(s):
...     return s.lower() == 'yes' and isinstance(s, str)

The function will still crash for numbers, because it will still try to call their lower method, which they don't have:

>>> is_yes(1)
AttributeError: 'int' object has no attribute 'lower'

However, if we put the type check first, we know that the lower method is not going to be called unless the type check passes, and the argument is indeed a string — and that's thanks to the short circuitry of and:

>>> def is_yes(s):
...     return isinstance(s, str) and s.lower() == 'yes'

>>> is_yes(1)
False

Similarly, if some function accepts an argument that can be None, then it can't use it, like by calling method, right away. This will crash for None, which has no methods, in what's known as a null dereference:

>>> def f(a):
...     if not a.is_valid() or a is None:
...         print('invalid argument')

>>> class A:
...     def is_valid(self):
...         return False

>>> a = A()
>>> f(a)
invalid argument
>>> f(None)
AttributeError: 'NoneType' object has no attribute 'is_valid'

Instead, we should check whether the argument is None first, and only if it isn't — that is, if the expression is still going on, according to the short circuitry of or — only then do we go on with the dereference, the specific method call:

>>> def f(a):
...     if a is None or not a.is_valid():
...         print('invalid argument')

>>> f(a)
invalid argument
>>> f(None)
invalid argument

More Than Boolean

Short circuitry makes a lot of sense, so a lot of languages have it. But Python goes one step further: it doesn't just return True or False as soon as possible — shorting the curcuit, so to speak — but returns the actual object, the "guilty party" at which the evaluation stopped.

If we look at it solely from the boolean perspective, it's completely legit. For and, either we arrive at some false object and stop there — in which case, the expression is false, and returning this object will be just as good; or, we get all the way to the end, and everything is true, in which case the last object is true as well.

For or, if we stop in the middle because some object is true, then the entire expression is true, so returning it will have equal effect; and if we went through everything and it's all false, then in particular the last object is false as well, and that's what we get.

So the fact that conditions don't necessarily evaluate to True or False, but to an actual object, is coherent at the very least; the returned object always has the right truth value. Hopefully, at this point, this should make sense:

>>> 1 and 10
10
>>> [] and 2
[]
>>> 5 or 8
5
>>> 0 or {}
{}

The only question that remains is why do we get the actual object and not just its truth value — what is it good for?

The or Trick

Well, with or, it lets us do the following trick: suppose some value might be missing — like, be None or an empty string — in which case some other, default value is used instead:

>>> def hello(name):
...     if not name:
...         name = 'stranger'
...     print('hello', name)

>>> hello('alice')
hello alice
>>> hello(None)
hello stranger

In this case, we explicitly check and reassign it if necessary. But we can also write it in a more compact (and arguablly, more readable) way, like so:

>>> def hello(name):
...     print('hello', name or 'stranger')

>>> hello('alice')
hello alice
>>> hello(None)
hello stranger

That is, if name is defined — if its truth value is True — then we use it; the rest of the expression doesn't matter, remember? And if its truth value is False, then we fall back to the "default" value after the or; in this case 'stranger'.

So in essence, this lets us have expressions of the form result = value or default.

The and Trick

For and, we have a different trick: suppose some object, like a log, can be missing; but when it's present, we want to use it, like to log certain messages:

>>> def compute(log):
...     if log:
...         log('starting...')
...     x = 1 # complicated calculation
...     if log:
...         log('computed x:', x)
...     y = 2 # complicated calculation
...     if log:
...         log('computed y:', y)
...     return x + y

If we pass in None, the computation takes place silently:

>>> compute(None)
3

But if we pass in the print function, it is used to trace the process as it unfolds:

>>> compute(print)
starting...
computed x: 1
computed y: 2
3

In this case, we precede every usage with a check that the log is defined, and only use it if it is; and while it works, it does get clunky. What if instead — and maybe it's a bit unusual, but definitely more compact — we'd precede every usage with the object in question, followed by and:

>>> def compute(log):
...     log and log('starting...')
...     x = 1
...     log and log('computed x:', x)
...     y = 2
...     log and log('computed y:', y)
...     return x + y

If the object's truth value is False, the whole expression — or in this case, statement — is false already, so its substance, the usage, is never executed; and if it's True, well, then we move on to the second part and effectively use our object as desired.

In other words, this lets us have expressions of the form value and value.method().

The Ternary Operator (Trick)

These and/or tricks are also a nice prelude into Python's story of the ternary operator.

In languages like Java and C, there's a shorthand for writing an if-else statement as an expression, usually like this: condition ? then_value : else_value. It's often the only operator that takes three arguments, so it's simply called the ternary operator.

Before version 2.4, Python didn't have one; but funnily enough, people figured out a creative way to achieve (almost) the same thing. Suppose we have:

>>> then_value = 1
>>> else_value = 2
>>> condition = True

So in effect, we aim to get 1. If we write:

>>> condition and then_value or else_value
1

It works! And if we negate the condition and aim to get 2:

>>> condition = False
>>> condition and then_value or else_value
2

It works just as well.

It's obscene, but what's happening is a combination of short circuitry and the precedence of boolean operators; more explicitly, it's the same as:

>>> (condition and then_value) or else_value

So first, we do condition and then_value — and only if it's false do we resort to the else-value, right?

Well, when condition is false, then the entire and-clause is false, and we get the else-value as expected. When condition is true, then the and-clause moves on to evaluate the then-value, and as long as it is true, there's no reason to evaluate the rest of the or, so we get the then-value as expected.

That's why I said it almost works: suppose the then-value is 0 — what would happen?

In this case, even when condition is true, and despite our cunning trickery, the overall and-clause still evaluates to 0, which is false, so we get the else-value after all:

>>> then_value = 0
>>> condition = True
>>> condition and then_value or else_value
2

The (Actual) Trenary Operator

To address this edge case, and generally to remedy this pretty confusing idiom, Python introduced a proper ternary operator.

Initially, the idea was to have something like condition then then_value else else_value; to keep the basic structure the same, but use more meaningful words and better defined behavior. Unfortunately, it ended up having a slightly different order: then_value if condition else else_value.

The reasoning was to avoid introducing then as a new keyword, which would break any older code that used it as a variable; better to reuse if, in which case the initial order doesn't make sense. But in my opinion, this way ended up more confusing, especially when then-values are so long that it obscures the conditional structure:

result = (some + very - elaborate * complicated / expression) if condition else value

Anyway, that's the ternary operator we have today:

>>> then_value = 1
>>> else_value = 2
>>> condition = True
>>> then_value if condition else else_value
1
>>> condition = False
>>> then_value if condition else else_value
2

And notice that even if the then-value is false, like 0, we still get it if the condition is true:

>>> then_value = 0
>>> condition = True
>>> then_value if condition else else_value
0

A Switch Statement?

Another conditional construct you might expect, coming from other languages, is the switch statement. Something along of the lines of:

switch argument:
    case value1:
        # case clause 1
        break
    case value2:
        # case clause 2
        break
    ...
    default:
        # default clause

For a long time, Python didn't have anything of this sort. And for a pretty good reason — it wasn't really necessary, as we could emulate this behavior quite easily with a chain of if and elifs:

if argument == value1:
    # case clause 1
elif argument == value2:
    # case clause 2
...

We could even use else for the optional default clause:

...
else:
    # default clause

And to get fall-through behavior, where several cases are grouped together, as in:

switch argument:
    ...
    case value3:
    case value4:
        # case clause 3-4
        break
    ...

We could simply group the expressions in the same if or elif statement with or:

...
elif argument == value3 or argument == value4:
    # case clause 3-4
...

Yeah, it's a bit less elegant; but I would argue it's not worth adding an entire language construct just to tidy it up.

Dispatch Dictionaries

Another cool trick we could use here, at least in some cases, is the dispatch dictionary. Suppose our different cases are encapsulated in different functions:

if argument == value1:
    case_1()
elif argument == value2:
    case_2()
...
else:
    default_case()

In this scenario, we can group these functions in a dictionary, mapping each case's value to that case's function; and then just index the dictionary with argument to dispatch the appropriate call:

{
    value1: case_1,
    value2: case_2,
    ...
}[argument]()

Mind the parentheses at the end, yeah? We have to actually invoke the function once it's selected.

As for the default case, we can emulate it by using the dictionary's get method. In contrast to direct, square-bracket indexing, which raises a KeyError when a key is missing, get is more tolerant, and returns None instead. This means we can use the or trick:

({
    value1: case_1,
    value2: case_2,
    ...
}.get(argument) or default_case)()

Or better yet, rely on the fact that get accepts an optional argument to be used as a default value in case the key is missing:

{
    value1: case_1,
    value2: case_2,
    ...
}.get(argument, default_case)()

In other words, we provide the default case as a fallback; to be returned, and invoked, if the argument didn't map to any of the other case functions.

The Match-Case Statement

In Python 3.10, the match-case statement was introduced; and here, I think it's good that they deviated from the standard name, "switch", because it actually does much more than just switch between different cases.

We can use it for that, of course — we can finally have a switch statement in Python, if we really want to, using an underscore for the optional, default case:

match argument:
    case value1:
        # case clause 1
    case value2:
        # case clause 2
    ...
    case _:
        # default clause

But like I said, it can actually do a lot more. For example, it supports all the assignment features we covered in the previous chapter, allowing for conditional data destructuring, where a clause happens only if its argument can be successfully unpacked as per its case requirements:

match argument:
    case []:
        print('empty list')
    case [x]:
        print('list with one item:', x)
    case [x, y]:
        print('list with two items:', x, y)
    case [x, y, *z]:
        print('list with more than two items:', x, y, '...')
    case _:
        print('not a list')

To be fair, the match-case statement is not so interesting in its conditionality (that is, its ability to switch between several cases) as for its ability to match structural patterns — so let's shelve it for now, and revisit it more fully in a future chapter.

Conditions: Conclusion

Like assignment, conditions are a deceptively simple topic — but they make so much of software, and are so fundamental to how it works, that they often reflect the broadest considerations of software design.

To sum up, we saw how objects have truth values, how these work with short circuitry and some and/or tricks, and how the ternary operator and the match-case statements came about. But throughout, and maybe more importantly, we started developing a common understanding of design — not as a set of rules to follow, but as a set of patterns to understand. Rules can be useful, and maybe comforting; but ultimately, they are not true, and they're not important. It's understanding why they were formulated to begin with that matters, because once we understand the reason behind a rule, we don't have to memorize anything about it, we can just derive it at will. What's more, we see when it doesn't apply, and can be broken for the sake of some compensating merit. At the end — and this is mastery — we can appreciate all those rules in context, and break them in such particular and expressive ways as to communicate new, and maybe deeper layers of meaning.

Conditions: Further Reading

  • Again, the standard documentation on conditions is pretty sparse. You can read the snippet on and , and check out the if-statement and ; there's also a bit called "" in the data structure section, but that's about it.
  • introduces the bool type to Python, and discuss its implications.
  • devises the ternary operator, and includes an interesting deliberation regarding its syntax and possible alternatives.
  • , also known as "", is a famous "poem" by core developer Tim Peters, which is actually baked into the Python interpreter as an easter egg (to see it, type in import this). It often serves as a springboard in discussions on writing "proper code" in a "Pythonic way", whatever that means — so it's as good a place as any to start building an intuition about good design (e.g. in GitHub Repository by Hunter Blanks). It went so far as to generate some academic papers (e.g. , or ), and even a . However, do take it with a grain of salt — particularly the line "special cases aren't special enough to break the rules". Given everything we've discussed (and that besides this vague poem, there aren't actually any well-defined rules to "Pythonicity"), I wouldn't take any of it too seriously — especially since it was primarily intended as a joke.
  • I also mentioned , by William Strunk Jr. and E. B. White; and while I find it to be an objectionable and overbearing litany, it does make some good points on English prose, and can inspire similar ideas about programming.