That's odd. What's going on here?

```
>>> x = 0
>>> numbers = [1, 1, 2, 3, 5, 8]
>>> for x in numbers:
... y = x**2
...
>>> y
64
>>> x
8
```

```
>>> x = 0
>>> numbers = [1, 1, 2, 3, 5, 8]
>>> squares = [x**2 for x in numbers]
>>> x
0 # In Python 2 this was 8
```

```
>>> NUMBERS = [1, 2, 3]
>>> def add_numbers(nums):
... NUMBERS += nums
...
>>> add_numbers([4, 5, 6])
Traceback (most recent call last):
File "
```", line 1, in
File "", line 2, in add_numbers
UnboundLocalError: local variable 'NUMBERS' referenced before assignment

```
>>> NUMBERS = [1, 2, 3]
>>> def add_numbers(nums):
... NUMBERS = NUMBERS + nums
...
>>> add_numbers([4, 5, 6])
Traceback (most recent call last):
File "
```", line 1, in
File "", line 2, in add_numbers
UnboundLocalError: local variable 'NUMBERS' referenced before assignment

```
>>> NUMBERS = [1, 2, 3]
>>> def set_numbers(nums):
... print(NUMBERS)
... NUMBERS = nums
...
>>> set_numbers([4, 5, 6])
Traceback (most recent call last):
File "
```", line 1, in
File "", line 2, in set_numbers
UnboundLocalError: local variable 'NUMBERS' referenced before assignment

```
>>> NUMBERS = [1, 2, 3]
>>> def set_numbers(nums):
... NUMBERS = nums
... print(NUMBERS)
...
>>> set_numbers([4, 5, 6])
[4, 5, 6]
>>> NUMBERS
[1, 2, 3]
```

```
>>> NUMBERS = [1, 2, 3]
>>> def add_numbers(nums):
... NUMBERS.extend(nums)
... print(NUMBERS)
...
>>> add_numbers([4, 5, 6])
[1, 2, 3, 4, 5, 6]
>>> NUMBERS
[1, 2, 3, 4, 5, 6]
```

- Reading global variables is perfectly fine
- But don't try to assign to them from a local scope
- List comprehensions have their own scope, loops don't
- Python has no block-level scoping
- Scope matters with assignment, not with mutation

```
>>> numbers = [1, 2, 3]
>>> numbers2 = numbers
>>> numbers2.append(4)
>>> numbers2
[1, 2, 3, 4]
>>> numbers
[1, 2, 3, 4]
>>> id(numbers)
139670455619848
>>> id(numbers2)
139670455619848
>>> numbers is numbers2
True
```

```
>>> x = ([1], [4])
>>> x[0].append(2)
>>> x
([1, 2], [4])
>>> y = x[0]
>>> y.append(3)
>>> x
([1, 2, 3], [4])
```

```
>>> x = []
>>> x.append(x)
>>> x
[[...]]
```

- Lists and dictionaries don't "contain" objects, they contain references (pointers) to objects
- Variables in Python are not buckets that contain things
- Variables are names that point to objects
- Assigning to a variable changes what object it points to
- Mutating an object changes the object itself
- Read Overlooked facts about variables and objects in Python: it's all about pointers
- Watch Facts and Myths about Python names and values
- Watch Names, Objects, and Plummeting From The Cliff

```
>>> duck_list = ['mallard']
>>> duck_list += ('eider', 'Pekin')
>>> duck_list
['mallard', 'eider', 'Pekin']
>>> duck_tuple = ('Muscovy', 'mandarin')
>>> duck_tuple += ['ruddy', 'Indian Runner']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate tuple (not "list") to tuple
```

```
>>> ducks = ('Muscovy', 'Indian Runner')
>>> ducks += ('mallard', 'ruddy')
>>> ducks += ['eider', 'Pekin']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate tuple (not "list") to tuple
```

```
>>> things = []
>>> things += "duck"
>>> things
['d', 'u', 'c', 'k']
>>> things.extend(' quack')
>>> things
['d', 'u', 'c', 'k', ' ', 'q', 'u', 'a', 'c', 'k']
```

```
>>> " ".join(["hello", "there!"])
'hello there!'
>>> " ".join(("hello", "there!"))
'hello there!'
>>> " ".join("hello")
'h e l l o'
>>> dict(('ab', 'cd'))
{'a': 'b', 'c': 'd'}
```

- If it looks like a duck and quacks like a duck, it's a duck
- Don't type check: rely on specific behaviors instead
- "Iterable" and "callable" describe behaviors (not types)

- The list
`extend`

method accepts any iterable - The list
`+=`

operator also accepts any iterable of strings - The tuple
`+=`

operator only accepts another tuple - Python thinks in terms of
*behaviors*, not*types* - Embrace duck typing by thinking in terms of behaviors: iterable, callable, sequence, mapping, hashable
- More on iterables: Loop Better: a deeper look at iteration

```
>>> a = b = (1, 2)
>>> a += (3, 4)
>>> a
(1, 2, 3, 4)
>>> b
(1, 2)
>>> a = a + (3, 4)
```

```
>>> a = b = [1, 2]
>>> a += [3, 4]
>>> a
[1, 2, 3, 4]
>>> b
[1, 2, 3, 4]
```

```
>>> a = b = [0]
>>> a += b
>>> a, b
([0, 0], [0, 0])
>>> a = b = [0]
>>> a = a + b
>>> a, b
([0, 0, 0, 0], [0, 0])
```

```
>>> a = b = [1, 2]
>>> a += [3, 4]
>>> a, b
([1, 2, 3, 4], [1, 2, 3, 4])
>>> c = d = "Python"
>>> c += "!!!" # Same as: c = c + "!!!"
>>> c, d
('Python!!!', 'Python')
>>> c is d
False
>>> a is b
True
```

```
>>> x = ([1, 2],)
>>> x[0] += [3, 4]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> x
([1, 2, 3, 4],)
```

```
>>> x = ([1, 2],)
>>> x[0] = x[0].__iadd__([3, 4]) # x[0] += [3, 4]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> x[0]
[1, 2, 3, 4]
>>> x[0].__iadd__([5])
[1, 2, 3, 4, 5]
>>> x[0] = [1, 2, 3, 4, 5]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
```

- In-place addition (
`+=`

) and other "augmented assignment" operations perform assignments - In-place addition calls the
`__iadd__`

method which allows the object to mutate itself if it chooses to do so - In-place addition do the addition "in-place" whenever possible

- Understanding how variables, mutability, data structures, operators work is important
- It's important to understand how your language "thinks"
- If it looks like a bug, it might just be a misunderstanding
- Found something odd? Try to learn from it!

```
>>> '8' < 8
False
>>> '8' < 9
False
>>> '8' < 99999999999999999999999999999999
False
>>> [8] > 8
True
>>> [8] < '8'
True
>>> sorted([str(type(8)), str(type('8')), str(type([8]))])
["<type 'int'>", "<type 'list'>", "<type 'str'>"]
>>> 8 < [8] < '8'
True
```

```
>>> class A:
... @property
... def x(self):
... return 'x value'
...
>>> a = A()
>>> a.x
'x value'
>>> a.x = 4
>>> a.x
4
```

```
>>> class Cipher:
... alphabet = 'abcdefghijklmnopqrstuvwxyz'
... letter_a = alphabet[0]
... letters = {
... letter: ord(letter) - ord(letter_a)
... for letter in alphabet
... }
...
Traceback (most recent call last):
File "
```", line 1, in
File "", line 6, in Cipher
File "", line 6, in
NameError: name 'letter_a' is not defined

```
>>> class Cipher:
... alphabet = 'abcdefghijklmnopqrstuvwxyz'
... letter_a = alphabet[0]
... letters = dict([
... (letter, ord(letter) - ord(letter_a))
... for letter in alphabet
... ])
...
>>> Cipher.letters['a']
0
```

```
>>> numbers = [1]
>>> class A:
... numbers = [2]
... m = 3
... squares = [n**2 for n in numbers]
...
>>> numbers[0], A.numbers[0], A.m, A.squares[0]
(1, 2, 3, 4)
```

```
>>> class A:
... numbers = [2]
... for n in numbers:
... print(n**2)
...
4
>>> A.numbers
[2]
>>> A.n
2
```

```
>>> class A:
... numbers = [1, 2]
... m = 3
... squares = [n**m for n in numbers]
... print(squares[0])
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in A
File "<stdin>", line 3, in <listcomp>
NameError: name 'm' is not defined
```

```
>>> class A:
... numbers = [1, 2]
... def hi(): print(numbers)
... hi()
...
Traceback (most recent call last):
File "
```", line 1, in
File "", line 4, in A
File "", line 3, in hi
NameError: name 'numbers' is not defined

```
>>> VOWELS = "aeiou"
>>> class B:
... VOWELS += "y"
...
>>> B.VOWELS
'aeiouy'
>>> VOWELS
'aeiou'
```

```
>>> NUMBERS = [1, 2, 3]
>>> class MyFavoriteClass:
... NUMBERS += [4, 5, 6]
...
>>> MyFavoriteClass.NUMBERS
[1, 2, 3, 4, 5, 6]
>>> NUMBERS
[1, 2, 3, 4, 5, 6]
```

```
>>> x = 999
>>> y = 999
>>> x == y
True
>>> x is y
False
>>> x = 4
>>> y = 4
>>> x is y
True
```

```
lots_of_numbers = range(-1000, 1000)
the_same_numbers = range(-1000, 1000)
same_numbers = (
i
for i, j in zip(lots_of_numbers, the_same_numbers)
if i is j
)
print(*same_numbers, sep=", ")
```

```
-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64,
65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81,
82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98,
99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112
113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125,
126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138,
139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151,
152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164,
165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177,
178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190,
191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203,
204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216,
217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229,
230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242,
243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256
```

```
>>> x = 'a' * 10
>>> y = 'a' * 10
>>> x is y
True
>>> x = 'a' * 1000
>>> y = 'a' * 1000
>>> x is y
False
```

```
>>> x = 'a' * 1000
>>> y = 'a' * 1000
>>> m = x[:10]
>>> n = y[:10]
>>> m is n
False
>>> m, n
('aaaaaaaaaa', 'aaaaaaaaaa')
>>> m == n
True
```

`is`

operator?```
def from_node(node):
node_list = []
while node is not None:
node_list.append(node)
node = node.next
return node_list
def is_iterator(iterable):
"""Return True if the given iterable is also an iterator."""
return iter(iterable) is iterable
```

- Equality and identity are different
- Checking for equality is very common
- Checking for identity is not nearly as common
- Python includes optimizations that make for strange identity patterns with strings and numbers
- Checking for identity when you need equality causes bugs

```
>>> [0] * 3
[0, 0, 0]
>>> [[0] * 3] * 3
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]
>>> matrix = [[0] * 3] * 3
>>> matrix[1][1] = 1
>>> matrix
[[0, 1, 0], [0, 1, 0], [0, 1, 0]]
```

```
>>> row = [0, 0, 0]
>>> matrix = [row, row, row]
>>> row[1] = 2
>>> matrix
[[0, 2, 0], [0, 2, 0], [0, 2, 0]]
>>> matrix[1][1] = 1
>>> matrix
[[0, 1, 0], [0, 1, 0], [0, 1, 0]]
>>> matrix[0] is matrix[1] is row
True
>>> matrix = [row] * 3
```

```
>>> 'Python' in 'Python' in 'Python'
True
>>> 'Python' in 'Python'
True
>>> True in 'Python'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'in <string>' requires string as left operand, not bool
>>> 'Python' in 'Python' in 'Python'
True
>>> ('Python' in 'Python') in 'Python'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'in <string>' requires string as left operand, not bool
>>> 'Python' in ['Python'] in [['Python']]
True
>>> x = 25
>>> 10 < x < 20
False
```

```
>>> """multiline
... strings"""
'multiline\nstrings'
>>> """""strings with
... five quotes around them!"""""
'""strings with\nfive quotes around them!'
>>> """""strings with five
... quotes around them!"""""
'""strings with five\nquotes around them!'
```