A Python Crash Course for the Statically Typed Programmer
November 15, 2013

Python is a multi-paradigm (hybrid) language. It's fully object-oriented but has strong functional roots. Note that this isn't a beginner's tutorial but a quick reference for the language and its features that should allow you to write basic Python ASAP.

If you had taken any academic course that involves programming, Python will most likely resemble pseudo code to you

def factorial(n):
    if n <= 1:
        return 1
    else:
        return n * factorial(n - 1)

# or

def factorial(n):
    res = 1
    while n:
        res *= n
        n -= 1
    return res

You'll see inheritance and class diagrams along side with constructs imported from Haskell and LISP.

Python is dynamically-typed (as opposed to statically-typed) but has strong-typing (as opposed to Perl or Javascript)

Syntax reference (1)

# printing to stdout
print EXPR

# assignment
VAR = EXPR

# conditions
if COND:
    SUITE
[elif COND:
    SUITE]
...
[else:
    SUITE]

# for-loops
for VAR in ITERABLE:
    SUITE
    [break]
    [continue]

# while-loops
while COND:
    SUITE
    [break]
    [continue]

Syntax reference (2)

# try-except
try:
    SUITE
except EXCEPTION [as ex]:
    SUITE
...
[else:                      # iff there was no exception
    SUITE]
[finally:                   # always be performed
    SUITE]

# raising exceptions
raise EXCEPTION(...)

# importing modules or attributes
import MODULE
from MODULE import NAME

# defining functions
def NAME([a, [b, ...]]):
    SUITE
    [return EXPR]
    [yield EXPR]

# defining classes
class NAME([BASE, [...]]):
    SUITE

The interactive interpreter is your friend! I mapped F11 on my keyboard to fire up an interpreter... it's better than any calculator you'll ever have

$ python
Python 2.7.5 (default, May 15 2013, 22:43:36) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> 5 + 6
11

For people with a C background:

  • You don't declare variables - you just assign them
    • And you can reassign them to different types
  • Python doesn't have do-while (use while) nor does it have switch (use ifs or dispatch tables)
  • Assignment is a statement, not an expression.
    • You cannot write if (a = foo()) == 5:
  • There's no such thing as passing by-value. It's always by-reference.
    • So you might need to explicitly copy objects some times
    • Or (preferably) create new objects from existing ones

For people with a C++/Java background:

  • Everything is a first-class object
    • Integers, functions, types, stack frames, tracebacks, etc.
  • There are no privates, only conventions. Members that start with _ are not to be manipulated directly.
  • There's no new, just invoke the class like a function.
    • inst = MyClass(1, 2, 3)

Duck Typing goes Nuclear

In low-level programming languages, types dictate a memory layout. In high-level languages, on the other hand, compile-time types are merely constraints on what you're allowed to do with an object.

Being an interpreted language, Python gives up on type-checking and instead adopts the spirit of "it's easier to ask for forgiveness than permission". Just try and see what happens.

>>> def foo(this, that):
...     return (this + that) * 2
...
>>> foo(3, 5)
16
>>> foo("hello", "world")
'helloworldhelloworld'

Python comes with lots of useful built-in types

>>> 5+3
8
>>> 27**63
149939874158678820041423971072487610193361136600
3344657118522818557991334322919287339806483L
>>> 5+3j
(5+3j)
>>> 1/(5+3j)
(0.14705882352941177-0.08823529411764706j)
>>> [1,2,3]
[1, 2, 3]
>>> [1,2,3] + [4,5,6]
[1, 2, 3, 4, 5, 6]
>>> (1,2,3)
(1, 2, 3)
>>> d={"a":4, 7:()}
>>> d["a"]
4
>>> d[7]
()
>>> True, False, None
(True, False, None)
>>> set([2,6,2,7,2,8,6])
set([8, 2, 6, 7])

String manipulation is a pleasure with Python

>>> "hello" + "world"
'helloworld'
>>> a="this is a single line string"
>>> a[5]
'i'
>>> a[5:12]
'is a si'
>>> a.upper()
'THIS IS A SINGLE LINE STRING'
>>> a.count("i")
5
>>> a.replace("i", "j")
'thjs js a sjngle ljne strjng'
>>> a.startswith("thi")
True
>>> "single" in a
True
>>> "multiple" in a
False
>>> a.split()
['this', 'is', 'a', 'single', 'line', 'string']
>>> a.split("i")
['th', 's ', 's a s', 'ngle l', 'ne str', 'ng']

Encoding strings like a boss

>>> "hello".encode("hex")
'68656c6c6f'
>>> "hello".encode("utf16")
'\xff\xfeh\x00e\x00l\x00l\x00o\x00'
>>> "hello".encode("zlib")
'x\x9c\xcbH\xcd\xc9\xc9\x07\x00\x06,\x02\x15'

String interpolation

>>> "My name is %s. You %s, prepare to %s" % ("Inigo Montoya", 
... "killed my father", "die")
'My name is Inigo Montoya. You killed my father, prepare to die'
>>>
>>> "My name is %03d" % (7,)
'My name is 007'

And joining strings is surprisingly useful

>>> ":".join(["AA", "BB", "CC"])
'AA:BB:CC'

Multi-line strings

>>> b="""this is
... a multi
... line string"""
>>> b
'this is\na multi\nline string'
>>> b.splitlines()
['this is', 'a multi', 'line string']

If I had time to show you only three functions, they will be

  • help()
  • dir()
  • type()

Everything else you can discover on your own.

>>> help
Type help() for interactive help, or help(object) for help about object.
>>> dir()
['__builtins__', '__doc__', '__name__', '__package__']

>>> help(dir)
    dir([object]) -> list of strings

    If called without an argument, return the names in the current scope.
    Else, return an alphabetized list of names comprising (some of) the attributes

>>> dir("hello")
['__add__', '__class__', '__contains__', '__delattr__', '__doc__', '__eq__', '__for
_mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
nter', 'count', 'decode', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'in
index', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswi']

>>> type(5)
<type 'int'>
>>> type("hello")
<type 'str'>

Next, let's meet some types and learn how to convert (not cast) values from one type to the other

>>> int
<type 'int'>
>>> str
<type 'str'>
>>> list
<type 'list'>

>>> int(5.1)
5
>>> int("5")
5
>>> str(5)
'5'
>>> list("hello")
['h', 'e', 'l', 'l', 'o']

Types matter

>>> 5 + "6"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'
>>> 5 + int("6")
11
>>> str(5) + "6"
'56'

Repr(esenation)

>>> repr(5)
'5'
>>> repr("hello")
"'hello'"
>>> print "Hello %s" % ("foo\n\tbar",)
Hello foo
        bar
>>> print "Hello %r" % ("foo\n\tbar",)
Hello 'foo\n\tbar'

Lists

>>> range(10)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> range(10,20)
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
>>> range(10,20,2)
[10, 12, 14, 16, 18]
>>> range(20,10,-2)
[20, 18, 16, 14, 12]
>>> len([2,5,2,6,2,6])
6
>>> len("hello")
5
>>> a=[2,5,2,6,2,6]
>>> max(a)
6
>>> min(a)
2
>>> b=["what", "is", "this", "thing"]
>>> max(b)
'what'
>>> max(b, key = len)
'thing'

Slicing

>>> a=range(10,20)
>>> a[0]
10
>>> a[-1]
19
>>> a[3:7]
[13, 14, 15, 16]
>>> a[7:]
[17, 18, 19]
>>> a[-3:]
[17, 18, 19]
>>> a[:-3]
[10, 11, 12, 13, 14, 15, 16]

Lambda functions

>>> lambda x: x * 2
<function <lambda> at 0x0297EEB0>
>>> f = lambda x: x * 2
>>> f(6)
12
>>> map(f, range(10))
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
>>> filter(lambda x: x % 2 == 0, range(10))
[0, 2, 4, 6, 8]

Working with files

>>> f = open("/etc/profile")
>>> f.read(100)
'# /etc/profile: system-wide .profile file for the Bourne shell (sh(1))\n# and Bourne compatible shell'
>>> f.readline()
's (bash(1), ksh(1), ash(1), ...).\n'
>>> f.readline()
'\n'
>>> f.readline()
'if [ "$PS1" ]; then\n'
>>> list(f)[:4]
['  if [ "$BASH" ] && [ "$BASH" != "/bin/sh" ]; then\n', 
 '    # The file bash.bashrc already sets the default PS1.\n', 
 "    # PS1='\\h:\\w\\$ '\n", 
 '    if [ -f /etc/bash.bashrc ]; then\n']
>>> f.close()

Working with resources

>>> with open("/etc/profile") as f:
...     f.read(100)
...
'# /etc/profile: system-wide .profile file for the Bourne shell (sh(1))\n# and Bourne compatible shell'

The with block automatically closes the file. It's more general than this, of course, as it's not limited to files. It simply ensures that the necessary cleanup that's associated with the object will be performed when you finish the with block.

For example, you can use with to commit a transaction to a DB or do a rollback on failure, etc. It's a very useful pattern.

List comprehension: remember map and filter? Well, it's time to forget about them

>>> [x for x in range(10)]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> [x * 2 for x in range(10)]
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
>>> [x for x in range(10) if x % 2 == 0]
[0, 2, 4, 6, 8]
>>> [x * 2 for x in range(10) if x % 2 == 0]
[0, 4, 8, 12, 16]

Yo comprendo!

>>> [i * j for i in range(1,5) for j in range(1,5)]
[1, 2, 3, 4, 2, 4, 6, 8, 3, 6, 9, 12, 4, 8, 12, 16]
>>> [[i * j for i in range(1,5)] for j in range(1,5)]
[[1, 2, 3, 4], [2, 4, 6, 8], [3, 6, 9, 12], [4, 8, 12, 16]]
>>> [" ".join(["%3d" % (i * j,) for i in range(1,5)]) 
...     for j in range(1,5)]
['  1   2   3   4', '  2   4   6   8', '  3   6   9  12',
 '  4   8  12  16']

And the whole multiplication table in just one line!

>>> print "\n".join([" ".join(["%3d" % (i * j,) for i in range(1,11)])
...     for j in range(1,11)])
  1   2   3   4   5   6   7   8   9  10
  2   4   6   8  10  12  14  16  18  20
  3   6   9  12  15  18  21  24  27  30
  4   8  12  16  20  24  28  32  36  40
  5  10  15  20  25  30  35  40  45  50
  6  12  18  24  30  36  42  48  54  60
  7  14  21  28  35  42  49  56  63  70
  8  16  24  32  40  48  56  64  72  80
  9  18  27  36  45  54  63  72  81  90
 10  20  30  40  50  60  70  80  90 100

Chillex! Be lazy

>>> def myfunc():
...     yield 1
...     yield 2
...     yield 3
...
>>> g=myfunc()
>>> g
<generator object myfunc at 0x02A3A8F0>
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
>>> g.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Generators let us be as general as we wish while paying only for what we're actually using (lazy evaluation)

>>> def fib():
...     a = b = 1
...     while True:
...         yield a
...         a, b = b, a + b
...
>>> g = fib()
>>> g.next(), g.next(), g.next(), g.next(), g.next(), g.next(), g.next()
(1, 1, 2, 3, 5, 8, 13)

The itertools module has some nifty utilities that we can use

>>> import itertools
>>> list(itertools.islice(fib(),10))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
>>>
>>> list(itertools.takewhile(lambda x: x < 50, fib))
[1, 1, 2, 3, 5, 8, 13, 21, 34]
>>>
>>> list(itertools.combinations([1,2,3,4],2))
[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
>>>
>>> list(itertools.permutations([1,2,3]))
[(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]

Remember list comprehensions? Forget them too. Generator comprehensions are the new black!

>>> (x for x in range(10))
<generator object <genexpr> at 0x02A37F30>
>>> (x * 2 for x in range(10))
<generator object <genexpr> at 0x02A34670>
>>> (x * 2 for x in range(10) if x % 2 == 0)
<generator object <genexpr> at 0x02A37918>

Huh? Of course you won't see anything... you have to consume the generator in order for it to produce values.

>>> list(x * 2 for x in range(10) if x % 2 == 0)
[0, 4, 8, 12, 16]

In fact, list comprehensions are a syntactic sugar for list(generator comprehension)

List comprehensions, as the name suggests, build a list. This can be expensive some times, especially when you don't need the intermediate values. E.g., if you just want to get the sum of the elements, there's no need to actually hold all of them in memory. Generators are the key to efficient programming. For example, xrange is like range but returns a generator instead.

>>> sum(range(1000000000))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
MemoryError
>>> sum(xrange(1000000000))
499999999500000000L

Object Oriented, Too

Don't worry though. Python isn't all geared for functional programming. We have classes too!

>>> class Animal(object):
...     def __init__(self, name):
...         self.name = name
...     def drink(self):
...         print "*Gulp gulp gulp*"
...
>>> class Dog(Animal):
...     def drink(self):
...         Animal.drink(self)         # or super(Dog, self).drink()
...         print "*Tongue drips*"
...     def bark(self):
...         print "Woof woof woof"
...
>>> rex = Dog("Rex t3h Dawg")
>>> rex.drink()
*Gulp gulp gulp*
*Tongue drips*
>>> rex.bark()
Woof woof woof

Some notes for Java folks:

  • Constructors are inherited! Why wouldn't they?!
    • You can implement an exception class in Python in just one line: class MyException(Exception): pass
    • In Java/C# you have to reimplement all constructors as well
  • Python doesn't have function/method overloading
    • But given that most of the code is duck-typed anyway (and can introspect the received object at runtime) it doesn't make a real difference

But if we stopped here you'd think it's just a variation of Java. We're way cooler than Java!

For example, instances (such as rex above) are actually just dictionaries that hold the instance's attributes.

>>> rex.__dict__
{'name' : 'Rex t3h Dawg'}

That's why we can add attributes on the fly

>>> rex.tail = "recursion"
>>> rex.__dict__
{'name' : 'Rex t3h Dawg', 'tail' : 'recursion'}

But classes ain't no different! They are just dictionaries of their methods

>>> Animal.__dict__.keys()
['__module__', 'drink', '__dict__', '__init__', '__weakref__', '__doc__']
>>>
>>> def sleep(self):
...     print "Zzzzzz"
...
>>> Animal.sleep = sleep      # monkey-patching
>>> rex.sleep()
Zzzzzz

Now it's time to discuss special methods. You've seen them already when we invoked dir("hello") -- they take the form of __xxx__, and surprisingly or not, they make up most of what we've seen so far.

Virtually all language constructs map to special methods underneath:

  • a + b is actually a.__add__(b)
  • a[x] maps to a.__getitem__(x)
  • str(a) invokes a.__str__()
  • a.b translates to a.__getattr__("b")
  • f(a,b,c) runs f.__call__(a, b, c)
    • So yes, f.__call__.__call__.__call__(a, b, c) as well
  • Dog("foo") creates the instance using __new__ and initializes it in __init__

You can invoke them yourself, of course, they're just methods:

>>> (5).__add__(6)
11

But why would you do that? Don't be silly.

Remember I said everything is an object? Well, I meant it.

>>> Animal.__base__
<type 'object'>
>>> Dog.__base__
<class '__main__.Animal'>
>>> Dog.__mro__
(<class '__main__.Dog'>, <class '__main__.Animal'>, <type 'object'>)
>>> type(Dog)
<type 'type'>

And it does gets mindboggling

>>> type
<type 'type'>
>>> type.__base__
<type 'object'>
>>> type(type)
<type 'type'>
>>> type(object)
<type 'type'>

The MRO (method resolution order) is actually very important. It determines what happens when you resolve attributes (__getattr__) on an object. For instance, rex.foo will first try rex.__dict__, move up to Dog.__dict__ and then to Animal.__dict__, until we reach object.__dict__ where we'll fail. And that's the whole object model.

Dictionaries are also crucial to functions. A function is basically just code that evaluates in a local scope and has access to a global scope (module-level). Each of these scopes is... a dictionary. When you assign a variable, you basically insert an element into the scope dictionary.

>>> globals()
{'__builtins__': <module '__builtin__' (built-in)>}
>>> def f(a, b):
...     print locals()
...     c = a+b
...     print locals()
...
>>> globals()
{'__builtins__': <module '__builtin__' (built-in)>, 'f': <function f at 0x028CEEB0>}
>>>
>>> f(6,7)
{'a': 6, 'b': 7}
{'a': 6, 'c': 13, 'b': 7}

And no Python tutorial can do without

When in Doubt

>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.