Home
Course Guidelines
About the course Prerequite Material References
Python
Jupyter Notebooks Python overview
Exercises
Before the semester start: Installation and exercise setup Week 1: Introduction to Python and libraries Week 2: Vector representations Week 3: Linear Algebra Week 4: Linear Transformations Week 5: Models and least squares Week 6: Assignment 1 - Gaze Estimation Week 7: Model selection and descriptive statistics Week 8: Filtering Week 9: Classification Week 10: Evaluation Week 11: Dimensionality reduction Week 12: Clustering and refresh on gradients Week 13: Neural Networks Week 14: Convolutional Neural Networks (CNN's)
Tutorials
Week 1: Data analysis, manipulation and plotting Week 2: Linear algebra Week 3: Transformations tutorial Week 4: Projection and Least Squares tutorial Week 7: Cross-validation and descriptive statistics tutorial Week 8: Filtering tutorial Week 11: Gradient Descent / Ascent
In-class Exercises
In-class 1 In-class 2 In-class 10 In-class 3 In-class 4 In-class 8
Explorer

Document

  • Overview

Content

  • Follow along
    • The REPL
    • Getting help
  • Overview
    • Why Python for this course?
    • Dynamic typing
    • Duck typing
  • Question: How may duck typing be ei…
    • A note on speed
  • Language basics
  • Basic operations
    • Built-in types
    • Arithmetic
    • Boolean operations
    • Functions
      • Anonymous functions
    • Printing to the standard outpu…
      • String conversion
    • Comments
    • Python Keywords
    • Modules and imports
  • Control flow
    • If statements
    • While loops
    • For loops
      • Looping with an index
    • Loop control
  • Collections
    • Sequence types
    • Accessing sequence types
      • Slicing and negative indices
    • Modifying lists
    • Unordered collection types
    • Common operations on collectio…
  • Classes
    • Inheritance in Python
  • Exception handling
  • Conclusion and guidelines
    • Discussion about Python
    • Code style
    • General guidelines
  • More features
    • Important
    • Great but not necessary for ba…
    • Comprehensions
      • Lists
    • Sets and dictionaries
      • Generator comprehensions
    • Unpacking
    • Variable arguments
    • Functional tools
    • Functions as objects
  • Other stuff
    • Generators
    • Dunder methods and operator ov…
    • Decorators
    • Context managers
    • Type hints
    • Metaclasses
    • Asyncio
  • More about functions

Python overview

Follow along

We encourage you to actively follow along these with the code examples by typing them into a Python read-eval-print-loop (REPL) or an editor. We suggest using the following online editor which requires no installation: ReplIT

The REPL

  • Run python in the terminal to start the REPL.
  • Run python -i <script name> to start REPL with a script.
  • Actually, use ipython (same arguments) instead of the standard REPL. It has autocomplete and syntax highlighting.

Getting help

Python has a built-in function help for printing documentation during REPL sessions or in Jupyter Lab. For example:

help(print)
help(print)
Help on built-in function print in module builtins:

print(...)
    print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)
    
    Prints the values to a stream, or to sys.stdout by default.
    Optional keyword arguments:
    file:  a file-like object (stream); defaults to the current sys.stdout.
    sep:   string inserted between values, default a space.
    end:   string appended after the last value, default a newline.
    flush: whether to forcibly flush the stream.

Overview

  • Invented in 1990 by Guido van Rossum
  • Multiple paradigms
    • But everything is an object (literally)
  • Dynamically typed and garbage collected
  • Interpreted

Why Python for this course?

  • Works cross platform
  • Easy to use for experimentation
  • Support for numerical processing libraries (i.e. Numpy)
  • Support for most popular deep learning frameworks (PyTorch, Tensorflow)
  • Scientific visualisation tools
  • Wraps OpenCV

Dynamic typing

Python does not use static types like most traditional languages, i.e. the type of a variable is never declared. Instead, variables can contain objects of any type and are declared by assigning a value to it.

For example, the following will not compile in Java:

int a; a = 10; a = "hello";
int a;
a = 10;
a = "hello";

but its Python equivalent is completely legal:

a = 10 a = 'hello' print(a)
a = 10
a = 'hello'
print(a)
hello

Objects still have actual types though! In a sense, you could use dynamic typing in Java by making all variables and fields of the type Object !

Examples:

print(type(10)) print(type('hello'))
print(type(10))
print(type('hello'))
<class 'int'>
<class 'str'>

Thus you can still easily get type errors. For example:

a-'hello'
a-'hello'

The consequence: type errors are only discovered at runtime.

As software developers this might seem very disconcerting, but don't worry! If you like static typing or want to develop a large Python project, you should look into type hints .

Duck typing

We don't care about what an object is, only about what it can do. For example:

class A: def f(self): print('A') class B: def f(self): print('B') def func(obj): obj.f()
class A:
    def f(self):
        print('A')

class B:
    def f(self):
        print('B')
        
def func(obj):
    obj.f()

Because there are no static types, we can call func with any arguments we like, e.g.

func(A())
func(A())
A
func(B())
func(B())
B
func(0)
func(0)

Question: How may duck typing be either and advantage or disadvantage in certain situations?

  • List advantages/disadvantages.
  • Discuss how behaviour similar to the one shown can be achieved in a statically typed language like Java.

Discuss in groups of 2/3 people for 5 minutes

A note on speed

  • Python, being an interpreted language, is slow compared to Java, C, etc.
  • Computer Vision and Machine Learning generally require computationally intensive programs.
  • Why use Python then?

  • It has excellent integration to C/C++ and many libraries (Numpy, OpenCV, PyTorch) use this to speed up calculations.
  • People use Python as a proxy or glue language for combining the functionality of fast frameworks using easy to write code.

Language basics

  • Indentation instead of braces.
  • Newline separates statements.
  • Files are written as scripts, i.e. can contain classes, functions, and individual statements at the top level.
  • Strings are delimited by '' or "" .
  • Legal identifiers can contain alphanumeric characters as well as _ and many common unicode characters.
    • One exception: The first character cannot be a digit!
class MyClass: # Class definition def my_method(self, param): # Method definition return param def outer_function(): # Function definition print('outer') # Individual statements obj = MyClass() print(obj.my_method(2))
class MyClass: # Class definition
    
    def my_method(self, param): # Method definition
        return param

def outer_function(): # Function definition
    print('outer')

# Individual statements
obj = MyClass() 
print(obj.my_method(2))
2

Basic operations

Built-in types

  • int for whole numbers (no size limit)
  • float for real numbers (usually 64 bit)
  • str for strings
  • bool for booleans (True / False )
  • None is equivalent to null in Java (but not equal, it is a special type)

Arithmetic

  • + , - , * , % work as expected for numbers.
  • / performs float division, // performs integer division.
  • ** performs exponentiation.
  • Note: + is also used for concatenation of strings.
42 % 10
42 % 10
10 + 2*3 + 10**5
10 + 2*3 + 10**5
10 / 3
10 / 3
10 // 3
10 // 3
'hello' + ' world'
'hello' + ' world'

Boolean operations

  • Boolean literals are named True and False in Python. In general, all values evaluate to True in a boolean expression except a 0 , the empty string '' , empty collections, and None .
  • Logical operators are keywords (and , or , not ).
  • Comparisons > , < , >= , <= , == , != are the same as in Java.
  • Boolean operators & , | work the same as in Java.
bool({})
bool({})
'' or True
'' or True
0b101 & 0b011 # 0bxx is a binary literal
0b101 & 0b011 # 0bxx is a binary literal

Functions

Functions are defined in Python using the following syntax:

def <name>(<parameters>): <body>
def ():
    

<parameters> is a comma-separated list of parameter names. It is possible to specify default parameter values like so:

def <name>(a, b=2): <body>
def (a, b=2):
    
Note: All parameters without default values must precede parameters with default values.

Link to Python docs.

Anonymous functions

You can define anonymous functions in Python using the lambda syntax, for example:

func = lambda x: x**5 func(10)
func = lambda x: x**5
func(10)

Anonymous functions shouldn't be assigned to variables like in the example. Instead use them only in higher order functions.

Printing to the standard output

Use the built-in function print for printing strings or any object

print('hello') print(10) print([1, 2, 3]) print('abc', 'def')
print('hello')
print(10)
print([1, 2, 3])
print('abc', 'def')
hello
10
[1, 2, 3]
abc def

Python supports easy string formatting by prepending a string with f and sorrounding variables in {<var>} , e.g.

print(f'formatted {10}')
print(f'formatted {10}')
formatted 10

It is possible to add format options. Link to tutorial.

String conversion

You can convert objects to strings using the built-in str() function (also used by print ). If you want to know how to define custom formatters for you own objects, see here .

Comments

  • Line comments start with # . No block comments.
  • Modules, functions and classes can include a documentation string as the first line of its definition, e.g.:
def my_function(x, y): """This is the docstring. This function does blah blah blah.""" # This is a regular comment ...
def my_function(x, y):
    """This is the docstring. This function does blah blah blah."""
    # This is a regular comment
    ...
  • The docstring is used by many IDEs and debuggers. It is also the text shown when calling help on a function:
help(my_function)
help(my_function)
Help on function my_function in module __main__:

my_function(x, y)
    This is the docstring. This function does blah blah blah.

Python Keywords

Python reserves the following keywords. That means they can not be used as identifiers!

keywords

Modules and imports

Python code is organised into modules which are essentially a bunch of files in a directory. A single python file is itself a module. Modules are imported using the import keyword. For example:

import datetime datetime.datetime.now()
import datetime

datetime.datetime.now()

Specific classes, functions or submodules can imported using the following syntax:

from datetime import datetime datetime.now()
from datetime import datetime

datetime.now()

Imports can also be renamed. This is often desirable to keep a clean namespace:

import datetime as dt dt.datetime.now()
import datetime as dt

dt.datetime.now()

Control flow

Python's control flow statements are very similar to ones from Java and C. The major difference, aside from not using curly braces is that parentheses are not needed for the expression to be evaluated.

If statements

x = 2
x = 2
if x == 3: print("X equals 3.") elif x == 2: print("X equals 2.") else: print("X equals something else.") print("This is outside the 'if'.")
if x == 3:
    print("X equals 3.")
elif x == 2:
    print("X equals 2.")
else:
    print("X equals something else.")
print("This is outside the 'if'.")
X equals 2.
This is outside the 'if'.

Note that else if is shortened to elif . They are both perfectly legal in Python although you should always use elif !

While loops

These are identical to the Java/C version except the syntactic differences:

while x > 0: x -= 1 print(x)
while x > 0:
    x -= 1
print(x)
0

For loops

For loops are a bit different in Python compared to Java. There is only one version with the syntax:

for <item> in <iterator>: ...
for  in :
    ...

Regular counting loops are recreated using the range function (docs ) which generates a sequence of integers in a specified range:

for i in range(5, 10, 2): print(i)
for i in range(5, 10, 2):
    print(i)
5
7
9

Looping over collection elements is very similar to Java's foreach loop

for e in ['a', 'b', 'c']: print(f'letter: {e}')
for e in ['a', 'b', 'c']:
    print(f'letter: {e}')
letter: a
letter: b
letter: c
for c in 'abc': print(f'letter: {c}')
for c in 'abc':
    print(f'letter: {c}')
letter: a
letter: b
letter: c

Looping with an index

Sometimes you want to use both the current item and its index in a given iteration. Instead of using range, use enumerate :

for i, e in enumerate(['a', 'b', 'c']): print(i, e)
for i, e in enumerate(['a', 'b', 'c']):
    print(i, e)
0 a
1 b
2 c

We will return to the i, e notation later.

Loop control

Python uses the keywords break and continue similarly to Java/C for stopping and skipping loop iterations.

Collections

Python has four main collection classes:

  • Sequences
    • Lists (arrays)
    • Tuples (immutable arrays)
    • Strings (character arrays)
  • Sets
  • Dictionaries (a map)

Sequence types

Lists are mutable and are defined using square brackets:

[1, 'a', 3]
[1, 'a', 3]

Tuples are immutable and are defined using parentheses:

('hello', 1.2)
('hello', 1.2)

Strings are immutable and are defined using quotas ('' or "" ):

'hello' + " world"
'hello' + " world"

or as multiline strings using triple quotes:

"""I am a multiline string."""
"""I am
a multiline
string."""

Accessing sequence types

Accessing a single element is similar to Java/C:

a = [1, 2] a[0]
a = [1, 2]
a[0]
b = ('a', 'bc') b[1]
b = ('a', 'bc')
b[1]
c = 'hello' c[3]
c = 'hello'
c[3]

Slicing and negative indices

A sublist can be created using a special slicing syntax a[<start>:<stop>] :

a = [1, 2, 3, 4, 5] a[2:4]
a = [1, 2, 3, 4, 5]
a[2:4]

Negative indices are used to access elements from the back of a list/tuple:

a[-2]
a[-2]

Modifying lists

Lists are actually arrays with automatic resizing like ArrayList in Java and allow appending in amortised constant time and access in constant time.

Changing values use the same syntax as Java/C:

a = [1, 2, 3, 4] a[1] = 5 a
a = [1, 2, 3, 4]
a[1] = 5
a

It is also possible to assign a list to a slice:

a[1:3] = [4, 8] a
a[1:3] = [4, 8]
a

Appending is done using the method append :

a.append(2) # Note that this is changes the list! a
a.append(2) # Note that this is changes the list!
a

Unordered collection types

Sets contain unique items in no particular order. They are equivalent to HashSet in Java.

s = {2, 3, 4} s
s = {2, 3, 4}
s

Dictionaries contain key-value pairs indexed by the key. They are equivalent to HashMap in Java.

d = {'key1': 0, 'key2': 3} d
d = {'key1': 0, 'key2': 3}
d

Common operations on collections

  • The len function is used to get the length of a collection.
  • The in keyword is used to check existence of an element, e.g. 2 in [2, 3, 4] .
  • All collections can be used in for loops.
  • Dictionaries have methods .items() and .values() for iterating over key-value pairs and values respectively.

Classes

Classes use the following syntax for declaration:

class <name>(<parent>): def __init__(self, <parameters>): self.<name> = <value> # Instance variable / field <body> def <name>(self, <parameters>): <body>
class ():
    
    def __init__(self, ):
        self. =  # Instance variable / field
        
       
    def (self, ):
        

  • The parentheses after the name of the class can be ommitted if it does not inherit from any parent.
  • The __init__ function is always the constructor in Python.
  • Note the use of the keyword self . It is equivalent to this in Java but is mandatory both as a first argument to methods and when accessing/creating instance variables.
  • To instantiate an object of a certain type, simply call the class name, i.e. <name>() . This is equivalent to calling new in Java.

Link to Python docs.

Inheritance in Python

  • Python has no interfaces or abstract classes!
  • Support for abstract classes through meta-programming (see here ).
  • Multiple inheritance instead of interfaces
    • Potentially weird stuff can happen
  • No method overloading!
class A: def __init__(self): print('A') super().__init__() class B: def __init__(self): print('B') super().__init__() class C(A, B): def __init__(self): print('C') super().__init__() C()
class A:
    def __init__(self):
        print('A')
        super().__init__()
        
class B:
    def __init__(self):
        print('B')
        super().__init__()
        
class C(A, B):
    def __init__(self):
        print('C')
        super().__init__()

C()
C
A
B

Exception handling

  • Works pretty much like Java (the syntax is a bit different)
  • No checked exceptions
try: a = '2' + 3 except TypeError as e: print('catched: ', e) finally: print('always runs')
try:
    a = '2' + 3
except TypeError as e:
    print('catched: ', e)
finally:
    print('always runs')
catched:  can only concatenate str (not "int") to str
always runs

You can except multiple errors in the following way:

try: ... except (A, B, C): ...
try:
    ...
except (A, B, C):
    ...

try: ... except A: ... except B: ...
try:
    ...
except A:
    ...
except B:
    ...

You can raise exceptions with:

raise ValueError("Should have been something else.")
raise ValueError("Should have been something else.")

You can define your own exceptions with:

class MyError(Exception): ... raise MyError('something')
class MyError(Exception):
    ...

raise MyError('something')

Conclusion and guidelines

Discussion about Python

  • What are your thoughts about the language? If you have previous experience, discuss it.
  • Compare Python to another language of choice. Focus on something you find important.

Discuss in groups of 2/3 people for 5 minutes

Code style

Python has an official style guide named PEP-8 which you should try to follow. Most editors and IDEs have built in support for it or supports linters (mypy, pylint, etc.) that have.

  • Use 4 spaces for indentation
  • Keep lines under 80 characters
  • Class names are camelcase, e.g. MyClass .
  • Function, method, and variable names are lowercase with words separated by underscores, e.g. convert_images .
  • Names of constants should be all caps with words separated by underscores, e.g. MAX_VALUE .

The complete guide is avaiable here .

General guidelines

import this
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.
Namespaces are one honking great idea -- let's do more of those!

Summary: Use the same guidelines you have learned for software development in general.

More features

Important

  • Comprehensions
  • Unpacking
  • Variable arguments
  • Generators
  • Functional tools

Great but not necessary for basic usage

  • Operator overloading
  • Decorators enables easy use of the decorator pattern
  • Context managers are great for managing context sensitive code, e.g. file io.
  • asyncio makes concurrency in Python easy
  • Metaclasses

Comprehensions

Python has a special syntax, called comprehensions, for performing common operations on collections. These are similar to comprehensions in other languages such as Haskell. They are essentially a filter and map operation combined.

Lists

List comprehensions are the most common and have the following syntax:

a = [2, 3, 4, 5] [str(e) for e in a]
a = [2, 3, 4, 5]
[str(e) for e in a]

It is possible to add guards to a comprehension like so:

[e for e in a if e%2==0]
[e for e in a if e%2==0]

Sets and dictionaries

A similar syntax can be used on sets and dictionaries:

a = {2, 3, 4}
a = {2, 3, 4}
{str(e) for e in a}
{str(e) for e in a}
{str(e): e**2 for e in a}
{str(e): e**2 for e in a}

Generator comprehensions

A lazy form of list comprehension can be created by exchanging the [] sorrounding the expression with () :

g = (e for e in a if e%2==0) g
g = (e for e in a if e%2==0)
g
list(g)
list(g)

Unpacking

Python supports a smart syntax for easy packing and unpacking of multiple values.

a, b = (2, 3) print(f'a: {a}, b: {b}')
a, b = (2, 3)
print(f'a: {a}, b: {b}')
a: 2, b: 3

This can be used for easy swapping of values

b, a = a, b print(f'a: {a}, b: {b}')
b, a = a, b
print(f'a: {a}, b: {b}')
a: 3, b: 2

All iterables (collections and strings) can be unpacked in this manner!

a, b = 'ab' print(f'a: {a}, b: {b}')
a, b = 'ab'
print(f'a: {a}, b: {b}')
a: a, b: b
s = {'a': 1, 'b': 2} a, b = s print(f'a: {a}, b: {b}')
s = {'a': 1, 'b': 2}
a, b = s
print(f'a: {a}, b: {b}')
a: a, b: b
a, b = s.items() # get key-value pairs! print(f'a: {a}, b: {b}')
a, b = s.items() # get key-value pairs!
print(f'a: {a}, b: {b}')
a: ('a', 1), b: ('b', 2)
# skip def mygen(): for i in range(5): yield i a, b, c, d, e = mygen()
# skip
def mygen():
    for i in range(5):
        yield i

a, b, c, d, e = mygen()

You can force unpacking in functions by prepending an iterable variable with * . E.g.

def myfunc(a, b): print(a, b) data = ('abc', 'def') myfunc(*data)
def myfunc(a, b):
    print(a, b)

data = ('abc', 'def')
myfunc(*data)
abc def

Variable arguments

Python supports two different methods for accepting a variable number of function/method arguments. First is unnamed arguments:

def vararg(*args): print(args) vararg(2, 3, 4)
def vararg(*args):
    print(args)

vararg(2, 3, 4)
(2, 3, 4)

Similarly, a function can accept a dictionary of named arguments:

def kwarg(**kwargs): print(kwargs) kwarg(a=1, b=2)
def kwarg(**kwargs):
    print(kwargs)

kwarg(a=1, b=2)
{'a': 1, 'b': 2}

Note that the names args and kwargs are simply conventions, although they are very strong conventions.

Functional tools

Python supports most common operations usually associated with functional languages. Closures are supported as lambda functions. The functions map , zip , and filter are built-in but more functions are accessible through the functools module.

Here's an example using map :

a = range(10) b = map(str, a) b
a = range(10)
b = map(str, a)
b

The functional tools in Python are lazy and thus we need to force the calculation to get the result:

list(b)
list(b)

zip allows you to combine two iterables into a new iterable with tuples containing elements from both inputs. E.g.

idx = range(5) char = 'ABCDE' zipped = zip(idx, char) list(zipped)
idx = range(5)
char = 'ABCDE'
zipped = zip(idx, char)
list(zipped)

Functions as objects

Functions in Python are, like everything else, objects. This means they are first-class citizens and can therefore be used to build higher-order functions by simply using a parameter as a function object.

Example:

def apply_print(func, v): print(func(v)) apply_print(len, 'abc')
def apply_print(func, v):
    print(func(v))

apply_print(len, 'abc')
3

Other stuff

Generators

The yield keyword is used to turn an ordinary function into a generator. The keyword returns the specified value and essentially pauses the function at that point. This is similar to the same keyword in C#. This is very useful to define custom iterators:

def mygen(start): while True: yield start start += 1 for i in mygen(10): print(i) if i>20: break
def mygen(start):
    while True:
        yield start
        start += 1

for i in mygen(10):
    print(i)
    if i>20:
        break
10
11
12
13
14
15
16
17
18
19
20
21

Dunder methods and operator overloading

Python objects support a number of special methods called dunder or magic methods. They allow access to modification of language features such as operator overloading.

  • You already know the __init__() method which is used for object initialisation.
  • Operators are overloaded using methods such as __add__ , __mul__ , etc.
  • Implementing the __call__ method allows you to call instances of an object.
  • Implementing __getitem__ and __setitem__ allows you to use the collection index/assignment syntax.

A simple example:

class Squares: def __getitem__(self, item): return item**2 s = Squares() s[500]
class Squares:
    def __getitem__(self, item):
        return item**2

s = Squares()
s[500]

Please don't ever do stuff like this! As with other languages supporting operator overloading, use it extremely sparingly.

Decorators

Decorators are supported in Python using an @<decorator-name> to decorate a function, method, or class. Typical built-in decorators are:

class MyClass: @staticmethod def method(a): print(a) @property def x(self): return 'a' @x.setter def x(self, value): print('setting x') obj = MyClass() obj.method(2) obj.x = 'a' obj.x
class MyClass:
    
    @staticmethod
    def method(a):
        print(a)
    
    @property
    def x(self):
        return 'a'
    
    @x.setter
    def x(self, value):
        print('setting x')

obj = MyClass()
obj.method(2)

obj.x = 'a'
obj.x
2
setting x

You can define your own decorators! They are simply functions or callable objects.

def my_decorator(func): def wrapper(*args, **kwargs): print('starting') func(*args, **kwargs) print('done') return wrapper @my_decorator def func(a): print('my function', a) # equivalent to def func(a): print('my function', a) func = my_decorator(func) func(2)
def my_decorator(func):
    def wrapper(*args, **kwargs):
        print('starting')
        func(*args, **kwargs)
        print('done')
    return wrapper

@my_decorator
def func(a):
    print('my function', a)

# equivalent to
def func(a):
    print('my function', a)
func = my_decorator(func)

func(2)
starting
my function 2
done

Context managers

IO operations require extra management due to the uncertainty of the operations. Specifically, it is often necessary to encapsulate code in try/catch blocks and manually close resources. Context managers makes this much easier by providing a keyword with used to create a block with automatic setup and finalising. For example:

with open(somefile) as file: file.read() # Do stuff
with open(somefile) as file:
    file.read()
    # Do stuff

You can create your own context managers as well! Link to docs

Type hints

Python supports type hints in the language. They are not handled by the interpreter though. For example:

def typed_func(a: str, b: int) -> str: return f'{a}{b}' typed_func(2,3)
def typed_func(a: str, b: int) -> str:
    return f'{a}{b}'

typed_func(2,3)

Why?? It's complicated and depends on who you ask.

Still:

  • Great tools exist for type checking (e.g. mypy ), some of which are built into IDEs like PyCharm.
  • Type hints are shown when using help and in most editor hints as well - this makes understanding an API so much easier.

My advice? Use type hints when creating anything to be reused.

Metaclasses

In Python, literally everything is an object. That means that even classes (yes the descriptions) are objects. When the interpreter reads a new class definition, a new class object is instantiated.

class MyClass(int): pass type(MyClass)
class MyClass(int):
    pass

type(MyClass)
MyClass.mro()
MyClass.mro()

This process of creating class objects can be modified by using metaclasses. They essentially allow modifications to the expected behavior of classes, making it possible to add functionality to the language.

A basic example:

class Meta(type): def __new__(cls, name, bases, attr): print(f'Creating new class "{name}"') print(f'inheriting from {bases}') return type.__new__(cls, name, bases, attr) class MyClass(str, metaclass=Meta): pass a = MyClass() b = MyClass()
class Meta(type):
    
    def __new__(cls, name, bases, attr):
        print(f'Creating new class "{name}"')
        print(f'inheriting from {bases}')
        return type.__new__(cls, name, bases, attr)

class MyClass(str, metaclass=Meta):
    pass

a = MyClass()
b = MyClass()
Creating new class "MyClass"
inheriting from (<class 'str'>,)

Asyncio

Python has a library asyncio for writing concurrent code using native syntax. Like C#, Python has the keywords async and await for specifying and controlling concurrent code. Link to docs

More about functions

  • Functions (named and anonymous) are all closures. They bind their enclosing environment.
  • Functions can be nested.

An example using both:

def create_printer(msg): def inner(): print(msg) return inner printer = create_printer(5) printer()
def create_printer(msg):
    def inner():
        print(msg)
    return inner

printer = create_printer(5)
printer()
5

Just to prove it to you:

printer.__closure__[0].cell_contents
printer.__closure__[0].cell_contents