Skip to content

Instantly share code, notes, and snippets.

@mportesdev
Last active October 31, 2020 18:18
Show Gist options
  • Save mportesdev/dd686534a0380817afe1d3d9bd49d21f to your computer and use it in GitHub Desktop.
Save mportesdev/dd686534a0380817afe1d3d9bd49d21f to your computer and use it in GitHub Desktop.
Little Python gotchas

Little Python gotchas

The last n elements

Suppose you want to get the last n elements of a sequence for several values of n:

>>> for n in (1, 5, 2, 0):
...     print('ABCDEFGH'[-n:])
...
H
DEFGH
GH
ABCDEFGH

The last iteration may have not printed out what we expected. The reason is quite obvious: sequence[-0:] is the same as sequence[0:], which means such a slice will produce a copy of the whole sequence.

Membership in iterables of characters

Suppose you have a simple function to restrict user input to certain permitted values:

def get_answer_from_options(message, options):
    while True:
        answer = input(message)
        if answer in options:
            return answer

As a container for the options, you can use e.g. a tuple:

>>> get_answer_from_options('yes or no: ', ('y', 'n'))
yes or no: boo
yes or no: n
'n'
>>> get_answer_from_options('yes or no: ', ('y', 'n'))
yes or no:
yes or no: y
'y'

When your options are just a few single-char answers like the above, you may decide to use a string as the options container:

>>> get_answer_from_options('yes or no: ', 'yn')
yes or no: boo
yes or no: n
'n'
>>> get_answer_from_options('yes or no: ', 'yn')
yes or no:
''

Not exactly the same behavior when the user enters an empty string. The reason is that char in ('y', 'n') and char in 'yn' are not entirely equivalent:

>>> '' in ('y', 'n')
False
>>> '' in 'yn'
True

Multiple assignment

Suppose you have the following list, and you want to swap two values, e.g. put bar where foo is, and vice versa. But you don't know their exact position in the list, so you use the index method.

>>> a = ['', '', 'foo', '', 'bar', '']

>>> a[a.index('foo')], a[a.index('bar')] = 'bar', 'foo'
>>> a
['', '', 'foo', '', 'bar', '']

Why didn't that work? The reason is that the two assignments are in fact executed separately, from left to right:

>>> a = ['', '', 'foo', '', 'bar', '']

>>> a[a.index('foo')] = 'bar'
>>> a
['', '', 'bar', '', 'bar', '']

>>> a[a.index('bar')] = 'foo'
>>> a
['', '', 'foo', '', 'bar', '']

Fun fact: should the order of elements in the original list be different, the multiple assignment would actually swap the values:

>>> a = ['', '', 'bar', '', 'foo', '']

>>> a[a.index('foo')], a[a.index('bar')] = 'bar', 'foo'
>>> a
['', '', 'foo', '', 'bar', '']

I think this makes this gotcha even more sneaky.

The in operator

The in operator checks for equality, not identity:

>>> a, b, c, d = [1], [2], [3], [1]

>>> for list_ in (a, b, c, d): print(id(list_))
... 
140460797957952
140460797958016
140460797958080
140460797957824
>>> a == d
True

>>> list_of_lists = [b, c, d]
>>> a in list_of_lists
True

>>> any(sublist is a for sublist in list_of_lists)
False
>>> id(a) in (id(sublist) for sublist in list_of_lists)
False

Similar example with custom objects:

>>> class A:
...     def __init__(self, a, b):
...         self.a = a
...         self.b = b
...     def __eq__(self, other):
...         return self.a == other.a and self.b == other.b
... 
>>> a, b, c, d = A(1, 2), A(3, 4), A(1, 2), A(5, 6)

>>> for obj in (a, b, c, d): print(id(obj))
... 
140691008330336
140691008331104
140691008331248
140691008331440
>>> a == c
True

>>> list_of_objects = [b, c, d]
>>> a in list_of_objects
True

>>> any(obj is a for obj in list_of_objects)
False
>>> id(a) in (id(obj) for obj in list_of_objects)
False

extend versus +=

Suppose you have a list called main in the global scope, and you need to add to it elements from another sequence. You write two functions that, at first glance, do the same thing:

>>> main = [1, 2, 3]

>>> def extend_main(sequence):
...     main.extend(sequence)
...
>>> def add_to_main(sequence):
...     main += sequence
...

The first function works as expected, while the other throws an error.

>>> extend_main([4, 5, 6])
>>> main
[1, 2, 3, 4, 5, 6]

>>> add_to_main([7, 8, 9])
UnboundLocalError: local variable 'main' referenced before assignment

Expressions in f-strings

>>> print(f'spam{" " * 4}eggs')
spam    eggs
>>> print(f'spam{"\n" * 4}eggs')
SyntaxError: f-string expression part cannot include a backslash
>>> newline = '\n'
>>> print(f'spam{newline * 4}eggs')
spam



eggs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment