Home
When nothing remains, everything is equally possible.
This guide hopes to take you from an intermediate Python programmer to a veritable Python McGyver, able to build anything out of practically nothing.
Hosted from this repo. Direct your blame here.
Whitespace
They'll tell you I'm insane, but I've got a blank space, baby, and I'll write your name.
You've probably heard that Python has significant whitespace, but whitespace has special significance to any esoteric pythonista.
Indentation
Unlike many programmers, Python won't fight you about whether you use tabs or spaces. If you replace all of the spaces and tabs in a Python file with each other, your code should still run. Using ·
for space and ↹
for tab, the following is valid Python:
if↹True:
·print(end="Hello")
·print("",↹"World")
You can combine both in one indentation level, if you're careful to always use the same mix of spaces and tabs:
if True:
↹·↹·↹a = 1
↹·↹·↹if a < 10:
↹·↹·↹···a = 10
↹·↹·↹···print(a)
However, the following will not work, because the order of the tab and space are different:
if True:
↹·a = 1
·↹b = 2 # IndentationError: unindent does not match any outer indentation level
Separators
Python is lax with whitespace, letting you put it in strange places and leave it out in stranger ones. Whenever Python thinks it can unambiguously figure out where a token ends, you don't need whitespace following. For instance:
print(arg1,arg2,kwarg=kwarg)
for(a)in[1]:b=a
print(1and 2in[]) #keywords and variables can't begin with digits
foo=lambda*args:lambda arg,/:args+[arg]
This is a godsend for golfing. Never put spaces around your operators, and try to manoeuvre your terms so that any keywords are right next to punctuation. For instance, checking if a number is odd, if 1-i%2:
can be if~i%2:
instead.
Numbers
Spaces can't go in numeric literals. Python views 1.__eq__
as (1.) __eq__
or (1.0) __eq__
, which is a syntax error. You can fix it with a space: 1 .__eq__
is the dunder method for == 1
because 1 .
isn't a valid float literal.
If you want to put a space in a long number for readability, you're reading the wrong guide. However, _
can be used instead, meaning 1_000_000
is a valid number.
Oddities
Numbers can be extremely misleading: [0x_for x in (1, 2, 3)]
evaluates to [15]
. It startles enough people that Python 3.10 has deprecated this kind of syntax misuse.
Expand for an explanation.
This means[0xF or (x in (1, 2, 3))]
. 0x_f
is a hexadecimal integer literal, and F in hex equals 15 as a decimal. o
isn't a hex digit, so Python doesn't lump or
into the literal. (15
is truthy, so using Boolean short circuiting, the x in (1, 2, 3)
is never executed. That means x
being undefined causes no error. The 15
is in a list literal rather than a list comprehension.) Quasi-operators
Whitespace can be a very confusing tool for things other than numbers. For instance, the so called 'pistol operator' or 'comma assignment' —
numbers = [42]
only_element ,= numbers
print(only_element) # 42
— is better rendered as only_element, = numbers
, (only_element,) = numbers
, or equivalently [only_element] = numbers
. The last makes it clear that the pistol operator uses iterable unpacking and irresponsible whitespace to extract the only element of a list. The 'space-invader increment operator' x -=- 1
is in a similar vein (parsing as x -= -1
).
Semicolons
Semicolons can be useful to avoid needing to type indentation characters; they act as a replacement newline for separating statements because Python and C are basically the same language.
while 2+2!=5: print("Try again."); break;
Both statements are part of the loop.
Practicalities
—Whitespace is great, but what does it mean for golfing?
I'm glad you asked. If you have only one indentation level, put it all on the same line. for x in y:a;b;c
If you have more than one of them, the outer indentation levels will need their own line. You don't need to use four spaces for each indent. A single space (or tab, you heathen!) will suffice.
if-1>a:
a
b
while c:
try:d
except:0
Truthiness and Falsiness
What's true for you doesn't have to be true forever, and sometimes the truth isn't always for the better.
Humanity has been trying to figure out the nature of truth for millennia. Philosophy tells us that some sentences are neither true nor false, such as this one. Fortunately, Python doesn't care what philosophers think.
Booleans
Booleans start simple enough: they can either be True
or False
. In the same way that 1 + 1
equals 2
, 1 + 1 == 2
equals True
. Python seems to respect mathematics more than philosophy, and there's a whole branch of algebra dedicated to combining True
and False
with the operators and
, or
and not
. (An interesting coincidence: the mathematician who invented Boolean algebra, George Boole, was named after Booleans.)
and
, not
and or
are pretty intuitive, though or
is inclusive, meaning True or True
is True
.
Expand for the full truth table.
Here is a lookup table for anyA
and any B
.
A | B | not A | not B | A and B | A or B |
---|---|---|---|---|---|
False | False | True | True | False | False |
False | True | True | False | False | True |
True | False | False | True | False | True |
True | True | False | False | True | True |
It is almost never necessary to write x == True
or x == False
. x
and not x
are cleaner, which may or may not be a good thing. You should also be careful when returning Booleans in if
statements:
if x is True:
return True
else:
return False
# returns True when x is True, and False when x is False
You can simply write return x
. You probably don't need to be warned that not not
is pointless.
Weirder
Perhaps you've noticed that Booleans look a lot like bits in binary. False
seems like 0
and True
like 1
. In fact, before Booleans were added, Python used 0
and 1
instead. This isn't just history trivia: Python still treats Booleans as numbers for backwards compatibility. Yes, that means True + 1
is 2
. You can use a bool
object any time you need an int
0
or 1
. This means you can write not x
as 1>x
, which is shorter and might cut out some whitespace around the expression.
Truthiness
What happens when Python expects a Boolean, but you give it a small archipelago? Probably not an error.
Python treats everything, not just Booleans, as either 'truthy' or 'falsy' (sometimes 'falsey'). If you put something truthy in an if
condition, it will run the block, but if it's falsy, the else
block will run instead.
Obviously True
is truthy, but every other object is also truthy by default. The exceptions are None
, zero, empty collections and, unsurprisingly, False
. The zeroes are 0
, 0.0
and 0j
. Empty collections include empty strings, empty lists, empty dictionaries, empty frozenset
s, empty tuples... you get the idea.
numbers = [1, 2, 3, 4, 5]
# loop until list is empty
while numbers: # truthy if numbers has elements, falsy otherwise
print(numbers.pop())
n = 10
# loop until n is 0
while n:
n -= 1
# the str version is no different
This makes conversion between Booleans and integers entirely implicit. You can use a XOR designed for integers on Booleans if you want: a ^ b
.
boolean<1
might seem like a short way to negate a Boolean, but if can be even shorter if you are prepared to do some setup first. The ~
operator converts 0
to -1
and -1
to 0
, among other things. -1
is not zero, so it's truthy. That means if you negate your Boolean beforehand (f = -f
, turning True
to -1
and False
to 0
) then you can simply use ~f
instead of not f
.
Algebra
Knowing Boolean algebra won't do you any harm but only one part is essential: De Morgan's Law. (not a) or (not b)
is always equal to not (a and b)
. Likewise, (not a) and (not b)
is not (a or b)
.
Expand for another way of putting it.
You probably use De Morgan's Law every day. If you are not rich and famous, then you are either not rich, or not famous. If you are not hungry or thirsty, then you must be both not hungry, and not thirsty.
In other words, if you have a not
outside an and
or or
expression, you can bring it inside of the expression by negating both operands a
and b
, then swapping and
with or
. Equivalently, you can negate both operands if you also negate the whole expression and swap the and
/or
. (Make sure you know where your brackets are, so you don't accidentally negate a
rather than the whole expression.) Here's a demonstration that not (A and B)
is (not A) or (not B)
:
A | B | not A | not B | A and B | not (A and B) | (not A) or (not B) |
---|---|---|---|---|---|---|
False | False | True | True | False | True | True |
False | True | True | False | False | True | True |
True | False | False | True | False | True | True |
True | True | False | False | True | False | False |
not (A or B)
is (not A) and (not B)
:
A | B | not A | not B | A or B | not (A or B) | (not A) and (not B) |
---|---|---|---|---|---|---|
False | False | True | True | False | True | True |
False | True | True | False | True | False | False |
True | False | False | True | True | False | False |
True | True | False | False | True | False | False |
Pragmatism
If you need to select an expression based on a Boolean, indexing (a,b)[boolean]
is often shorter than an explicit ternary conditional b if boolean else a
. The problem with the former is that it will always evaluate both a
and b
. For example, (print("a"),print("b"))[boolean]
will always print both a
and b
, so should be converted to print(("a","b")[boolean])
or print("ab"[boolean])
to print only one of the two. (Many other situations can be shortened using short-circuiting.)
If you need to convert a Boolean to lowercase text "true"
/"false"
, you can interleave the two strings and use a slice, saving on quote marks. "ftarlusee"[boolean::2]
takes every second character, starting at either 0
or 1
depending, so will suffice. The trick works for any pair of strings that are the same length, or if there's one extra character for the False
option.
Short Circuits
"Oh dear," says God, "I hadn't thought of that," and promptly vanishes in a puff of logic.
Programming is sometimes called the art of laziness, but Python has laziness down to a science.
Ternary conditionals
Imagine you are a Python coder. Just imagine it. If you are a Python programmer, this won't be particularly difficult. You might be able to imagine yourself coding an if
statement.
if condition:
x = func()
else:
x = 0
This can be shortened to one line using Python's handy-dandy ternary conditional syntax:
x = func() if condition else 0
The syntax for t if c else f
should be mostly self-explanatory, and it's an expression so it can be used in more positions than the traditional if:
/else:
. (The trade-off is that you can only put expressions in it, rather than full statements, but that is rarely a problem.)
print(f"You're {'quite'if...else'not'} correct!")
# prints "You're quite correct!"
and
and/or or
A little secret: most of the time, ternary conditionals are not necessary. We've seen that Python has Boolean operators.
a | b # a OR b
a & b # a AND b
a ^ b # a XOR b
~a # NOT a
Hold on, those aren't the operators from the previous page. They're bitwise operators, not Boolean. They shouldn't be here right now. Maybe if you ignore them they'll go away.
a or b
a and b
a != b
not a
That's better. Now then, these Boolean operators are capable of emulating if
and else
, because of an optimisation called 'short-circuiting'. (The bitwise operators don't have that power.)
Short circuiting
Look at an expression like True or foo()
. foo
might return True
or False
, but it doesn't matter which, because True
or
anything is always truthy. There's really no need to run foo
. Python realises this information, and will instantly return True
when it sees a True
on the left of an or
. Similarly, False and foo()
is always False
, so Python won't bother running foo
.
(If you like, you can confirm this with the truth table.)
In fact, a or b
is like a shorter version of a if a else b
, and a and b
is like a if not a else b
. (A slight difference: foo() or b
calls foo
once, while foo() if foo() else b
might double up on calls.) After staring at that notation, you might be wondering what happens with values that aren't True
or False
.
1 or 0/0
# = 1 (doesn't raise `ZeroDivisionError`)
"" and print("something")
# = "" (doesn't print anything)
print("a") or "b"
# = 'b' (prints 'a' as well)
As you can see, it doesn't need to return Booleans.
That last example has practical merit. The print
function, along with many other functions and methods that have side effects, returns None
, which is falsy. That allows you to chain operations together: my_list.append(number) or print(my_list) or number
is an expression. It evaluates to number
, but it also appends number
to a list and prints out that list.
If you know that a()
always evaluates to something falsy, you can emulate b() if c else a()
using (c or a()) and b()
. It may not be any shorter, but it can often suggest other shortenings.
For instance, let's say you need the result of a function, except some of the time it returns a falsy value (None
, 0
, ""
etc), and you need to replace those falsy return values with a default.
line = fizz_or_buzz(n) if fizz_or_buzz(n) != "" else str(n)
# equivalently:
line = fizz_or_buzz(n) or str(n)
Chained comparisons
Python automatically inserts the Boolean operator and
into certain expressions.
if 0 <= a < 10:
print("in range")
# equivalent to:
if 0 <= a and a < 10:
print("in range")
(Oh, alright. They're not quite equivalent, just like how Boolean operators aren't quite the same as ternary conditionals. The middle term is only evaluated once in a chained comparison, so 0 <= foo() < 10
would call foo
once but 0 <= foo() and foo() < 10
would call it twice.)
print(1 < 10 < 100 < 1000 < 10000)
# True
print(1 < 2 > 3)
# False
print(1000 < 42 <= 0/0)
# False (doesn't raise ZeroDivisionError because of Boolean short circuiting)
The comparison operators in Python are:
<
and>
==
and!=
<=
and>=
is
andis not
in
andnot in
You can write 3 in (1, 2, 3) < (1, 2, 4) > (1, 2) not in [] != 1 is 1
, which is truthy. For extra fun, it will raise a SyntaxWarning
.
Precedence
Python isn't a Lisp, so there's no reason to use extra brackets. If you keep in mind operator precedence, you can avoid adding redundant ones.
It can be easier to commit lists to memory with an explanation why they are in the order they are. You have probably seen that Python uses BEDMAS/PEMDAS/BODMAS/BIDMAS ordering, which means a + (b * c)
is just a + b * c
—Python always does the *
before the +
. None of these mnemonics have a letter for "Boolean AND" or "unary inverse" or "modulo", so it's just a skeleton of Python's operators.
If you'd like to follow along, there's a concise reference version in the Python docs.
B/P
-
First in the acronym is 'B' for 'brackets', because
(...)
is always the tightest grouping. Never can anything inside(...)
pair itself with anything outside the brackets. This also goes for square brackets[...]
and squiggly brackets{...}
. -
After that, there's another use of brackets, which is calling functions
foo(...)
and indexing listsfoo[...]
. Attribute access is also at this level of precedencefoo.bar
. -
Next is the
await
keyword. It's looser than the above, soawait lib.load("utf-8")
isawait ( ( lib.load )("utf-8") )
.The operators so far have all operated on one value. In
foo()
, the()
operates on justfoo
, whereasfoo + bar
, which comes later in the precedence chart, has+
combining bothfoo
andbar
together. As a general rule, the single-value operations (likeawait
) bind tighter than the two-value operators.
E/O
-
Every general rule has an exception, and exponentiation
**
is the exception to that one. It binds tighter than-foo
,~foo
and+foo
, even though they're single-value operators anda ** b
takes two values.It's also an exception to another rule: it's 'right-associative' rather than left-associative. That's another way of saying that Python treats
a ** b ** c ** d
asa ** (b ** (c**d))
, which is the opposite order to every other two-valued operator. For instance,a * b * c * d
turns into((a*b) * c) * d
. Exponentiation probably gets its right-associativity from mathematical notation, where a power law, (ab)c = ab×c, makes left-associative exponents pointless. -
Of course next are
-foo
,~foo
,+foo
. (These are not their two-value forms:+foo
is a different operator tofoo + bar
.) If you've ever typed-1**2
into Google and got confused why it's-1
rather than(-1)**2
=1
, this is why.
D/M
- We're up to the 'D' and 'M' for division and multiplication. Python treats
*
,@
,/
,//
and%
as forms of multiplication and division, so they all have the same precedence.
A/S
- 'A' and 'S'—adding and subtracting—are uncomplicated.
foo + bar
andfoo - bar
are the loosest operators yet.
Bitwise
-
The mnemonics end here, and this is the point where primary school students would stop learning and go outside. They didn't have to learn about bitwise operations, starting with left-shift
<<
and right-shift>>
. The shifts are at this precedence because they're often used to prepare values for the other bitwise operators. -
After that are
&
,^
and|
, in that order. The reason&
(bitwise AND) and|
(bitwise OR) go in this order, is that mathematicians think of&
and|
as a lot like*
and+
, respectively—that's whya * b
gives you the same Boolean asa & b
if (a
andb
are Booleans) andbool(a + b)
gives you the same asa | b
.So to keep the mathematicians happy,
a | b & c
has analogous operator precedence toa + b * c
: it'sa | (b & c)
. (For some reason bitwise XOR^
is put between these operators' precedence.)
Boolean
-
You've already had a look at the comparison operators. They all go here. That's why
a + b >= c
doesn't accidentally parse asa + (b >= c)
which would be less intuitive than(a + b) >= c
. Python combines all of the numerical and string operations at a tighter precedence than Booleans, because lots of Boolean expressions have numerical operations like addition inside them. -
Intuition says that
a >= b or b >= c
should parse as(a >= b) or (a >= c)
, so the Boolean operators go next. The first Boolean operator is the one-valued Boolean operator,not a
, because operations on a single value come before operations on two values. Then the two-valued operators,a and b
anda or b
. Just like&
and|
,and
binds more tightly thanor
does. -
What does Python expect you to do with Booleans?
—Use them in an
if
/else
!So
a if b else c
has to be looser than everything else so far.Did you know that 'ternary' in "ternary conditional" means "three-valued," because it takes an
a
, ab
and ac
? Given the pattern of single-valued operators before double-, it fits that a three-valued operator should go after all the other Boolean operations.
Beyond
- The penultimate on the list is
lambda ...: foo
, which is designed so that almost any expression can go on its right-hand-side. - The only expression you can't put on the right of a lambda is anything using walrus assignment,
:=
, because Python thought that would be useless.
Phew! That's all of them.
# Where might the brackets for this go then?
(f := lambda: 0 if 4 & -foo.bar()[1 ** 2 ** 3] + 1 % 2 >> 3 and 3 != 3 ^ 2 or False else baz)
Expand for an answer.
It should be something likef := (lambda: (0 if ((4 & (((-(((foo.bar)())[1 ** (2 ** 3)])) + (1 % 2)) >> 3) and (3 != (3 ^ 2))) or False) else baz))
Every so often you can use these precedence rules to shave off a few brackets. It helps to keep them with you always. Post-it them to the back of your laptop. Tattoo them on your wrist. Whisper them as you fall asleep.
Golfer's corner
Aren't Boolean operators nice? So much statelier than the bitwise operators, you may well find.
They are lazy though, it must be said. and
is most useful for giving up. If your data processing only makes sense on truthy values, and
will let you pass falsy ones through unprocessed. Though, as you've seen, and
is not as short as &
for comparing real Booleans, no matter how painful to admit.
and
might be better than &
if you need the implicit Boolean conversions, when you want to return an arbitrary object, for its short-circuiting behaviour, or if it has a helpful precedence that lets you trim brackets.
Credits
Most of this: IFcoltransG.
Inspiration: #esoteric-python
in the Python Discord, plus Tips for golfing in Python, and Learn You a Haskell.
Quotes
- Main page: a Pythonista on the
#esoteric-python
channel. - Home: One With Nothing.
- Whitespace: Blank Space by Taylor Swift.
- Truthiness: The Truth from the 36 Questions soundtrack.
- Short circuits: The Hitchhikers Guide to the Galaxy by Douglas Adams.
- Lambdas: generic template.
- Operators and Builtins: Batman (1967).