The python file extension is *.py

To execute (assuming Python is in your PATH variable) run "python" from the console.

Python is interpreted, not compiled. (Although it can be compiled.)
Python has garbage collection.
Python is dynamically typed. (The type of a variable can change during runtime, and type checking occurs at runtime.)
Python supports both functional and object-oriented programming paradigms.

These notes focus on Python 3, not Python 2.

Goals of the language:
- "there is only one way to do each thing"
- "focuses on simplicity"

the "eval" method will parse what is passed in as Python and run it

eval("2+2") #returns 4


Python commands usually terminate on an end-line.
Ending statements with a semi-colon (;) does not cause an error, but is against convention.
Blank lines (white-space characters, comments) are ignored.

You can stretch a command across multiple lines by escaping the end-line character

if x==1 and y==0 \
    and z==6 \
    and t==8 :

You can place multiple statements on a single line by ending each statement with a ;

x=1; y=4; u=7;


Significant Whitespace - whitespace characters matter to the syntax.
Python uses indents to define coding blocks, instead of braces.

x = 0;
if x == 1 :
print("c"); #only this is run

The indentation amount is not fixed, but must be consistent for all lines in a block. Indentation can be made up of tabs and/or spaces.

There are competing conventions for one-tab-per-indent and for x-spaces-per-indent.


Single line comments

    x = 7 #comments

There are no multiline comments in python, but here is the commonly accepted work-around. Wrap the section in triple quotes to turn it into a multiline string literal. The line is still interpreted, but does nothing.



def calc(a, b):
    return a + b

def main():
if __name__ == "__main__":
This allows the file to be used as a library or to be run directly. When run directly, the main function is run.

Command Line

Interactive Shell

Run "python" in a command prompt to start the interactive shell.
You can type and run python one line at a time.


To access command line arguments:

    import sys
    print(sys.argv) #an array of command line arguments
    print(sys.argv[0]) #the name of this script

Exit Script

    import sys


use PIP to install python packages so they can be imported into your script

verify PIP is installed (it comes with Python)

//in terminal
pip3 --version

download and install package "requests"

pip3 requests


How to organize your program into multiple files:

import B
print("this is A")

 #file in same directory
print("this is B")

 #output from running python
 #this is B
 #this is A

import B
B.myFunc() #must specify the module name to call the function
print("this is A")

 #file in same directory
def myFunc():
    print("this is B")

 #output from running python
 #this is B
 #this is A


import really________long_______name as shortName
import module.submodule.subsubmodule as otherShortName

Import Object

Don't want to say ""? You can import just the datetime object from the datetime module.

from datetime import datetime
currentDateTime =



Writing output to the console. Print has an implicit end-line character.
String literals can be written with single or double quotes.

 #python 2
print "text"

 #python 3

Print will concatenate strings together, with a default space (" ") delimitor.

x = 1
print("a", x) #outputs "a 1"

Formatted strings: everything inside a { } will be interpreted as a python expression in the current scope.

myCount = 5
myVar = "A"
print(f"count: {myCount}, var: {myVar}") #"count: 5, var: A"


The user will see a prompt at the command line. When the user hits Enter, whatever they just typed will be returned to you.

Input - converts user text to the (seemingly) appropriate type

x = input("Enter a number:") #user input is assigned to x

Raw Input - returns all input as a string

y = raw_input("Prompt:") #treats all input as string
y = int(y) #make sure of type #throws error if value cannot be converted to int

Cleaning input

x = ""
while not x.isdigit():
    x = input("Prompt: ")
x = int(x)


x = 5
y = "hey"
print(x) #5
print(y) #"hey"
print(str(x)+y) #"5hey"
print(f'{x} {y}') #"5 hey"

Variables are not declared. They are created when they are first assigned.

Naming Conventions

Begin with a letter or underscore
Continue with letters, numbers, or underscores
Names are case-sensitive
Only start with an underscore for special variables


x = 1
y = "a"
z = 1 * 5 + 7 / 4

x = y = z = 1 #all equal 1 now

x,y,z = 1,"a",4+5 #x=1, y="a", z=9

You can swap variables in a single line.

x,y = 1,2 #x=1 and y=2
x,y = y,x #now x=2 and y=1

Naming Collisions

Python will let you name variables the same as modules, as objects, as functions. If you do so, the variable overwrites the previous "value".

For example, if you name a variable "sum" and then try to use the built-in sum function, you'll get an error.


Python is loosely typed: it will automatically determine the variable type based on the value, and the type can change.
Type is inferred from the assigned value.

Everything in Python is an object, each with an identity, type, and value.
Some objects are mutable (value can be changed), some are immutable (value cannot be changed).

The types generally called "primitives" are all immutable objects in Python: integer, string, boolean, etc.
Some built-in classes are str, int, list, dict.

Collections and user-defined types are all mutable objects: list, set, dictionary, etc.

Memory Allocation

Everything is an object in python.
When Python interprets a function definition, it allocates memory for this new object, even if the function is never called.
When Python interprets a class definition, it allocates memory for this new object, even if no instances of the class are ever created.


Integer: a whole number

String: an ordered sequence of characters

Boolean: true or false

List: an ordered sequence of elements (mutable)
can contain different data types

Tuple: an ordered sequence of elements (immutable)
can contain different data types

Set: an unordered collection of unique elements
can contain different data types
Sets can only contain immutable objects - otherwise you get an "unhashable type" error
Sets have length, but not indexes.

Dict: an unordered collection of key/value pairs
can contain different data types


x = "hello"
x = str("hello")

y = 5
y = int(5)

Get Type

print(type(12)) #<class 'int'>

print(type(12.56)) #<class 'float'>
print(type(5E22)) #<class 'float'>

print(type(True)) #<class 'bool'>
print(type(False)) #<class 'bool'>

print(type(complex(2,5))) #<class 'complex'>
print(type(2+5J)) #<class 'complex'>

print(type("a")) #<class 'str'>
print(type('a')) #<class 'str'>

print(type(u"a")) #<class 'str'>
print(type(u'a')) #<class 'str'>

print(type((1, "a", True))) #<class 'tuple'>
print(type(1, "a", True)) #<class 'tuple'>

print(type([1, "a", True])) #<class 'list'>

print(type({ "name":"Bob", "degree":"business", "age":23 })) #<class 'dict'>

print(type(range(0,10))) #<class 'range'>


Mutable Types: List, Set, Dict

Immutable Types: Integer, Float, Complex, Boolean, String, Tuple, Frozenset


ascii(x)   #returns string representation of object?
bin(x)     #int to binary string
bool(x)    #converts to boolean
chr(x)     #int to character
complex(x) #constructs a complex number from arguments
float(x)   #converts int or string to float
hex(x)     #converts int to hexadecimal string
int(x)     #constructs an int from number or string
oct(x)     #converts into to octal string
ord(x)     #converts character to int
repr(x)    #returns string representation of object?
str(x)     #returns string representation of object?
type(x)    #returns the type of the object


Iterable Types: String, List, Tuple, Dict, Set, Frozenset

Unordered Types: Dict, Set, Frozenset

all(iterable) #returns True if all elements are True
any(iterable) #returns True if any elements are True

enumerate(iterable) #returns a list of tuples as (index, value)

filter(myFilter, iterable) #returns a list of the elements that passed the filter
 #the filter is a function that accepts one element and returns True or False

myIter = iter(collection) #returns an iterable object for the collection
a = next(myIter) #first value
b = next(myIter) #second value

len(collection) #returns the number of elements in the collection

map(myMap, iterable) #applies a function to each element of a collection and returns a list of the results
 #the map is a function that accepts one element and returns any value

reversed(iterable) #returns new collection in the reversed order
sorted(iterable)   #returns new collection in the sorted order

zip(iterableA, iterableB, ...) #returns a list of tuples made up of one element from each provided collection
a = ["A","B","C"]
b = [1,2,3]
c = zip(a, b) #equals [("A",1),("B",2),("C",3)]

Sequence Types

string, list, tuple, range, buffer, unicode string


    stringA = "abc"
    stringB = 'abc'
    stringRaw = r'\a\b\c' #"\a\b\c"

    tupleA = (1, 'a', True)
    #parentheses are optional
    tupleB = 1, 'a', True 
    #an empty tuple requires parentheses
    tupleC = ()
    #a single element tuple requires parentheses and a comma
    #or it will be interpreted as the simple type (int, in this case)
    tupleD = (1, )

    listA = [1, 'a', True]
    listB = []
    unicodeA = u"abc"
    unicodeB = u'abc'
    #buffer objects are not directly supported, they are returned by some functions
    #range objects are not directly supported, they are returned by the "range()" function

String escape characters:
\n newline
\t tab horizontal
\\ backslash
\' single quote
\" double quote

String, List, Tuple

concat strings with + operator


    if element in sequence:
    if "out" in "without":
    if 2 in [1,2,3]:
    if element not in sequence:
    concatA = sequenceA + sequenceB
    concatB = sequenceA sequenceB

    #concatenate x shallow copies of sequence together
    repeatedA = sequenceA * count
    repeatedB = count * sequenceB
    #get ith element, 0-based index
    elementA = sequence[i]
    #get ith through (j-1)th elements in a new sequence
    sliceA = sequence[i:j]
    #get ith through (j-1)th elements, with steps of k
    sliceB = sequence[i:j:k]
    length = len(sequence)
    minElement = min(sequence)
    maxElement = max(sequence)


    #0 origin
    s = "abcdefg"
    print(s[0]) #"a"
    print(s[-1]) #"g"


x = "0123456789"

 #boundaries default to start and end of sequence

print(x[2:5]) #234
print(x[-2:-5]) #prints empty line, cannot reverse a string this way
print(x[::-1]) #9876543210 #this is how you reverse a string
print(x[-5:-2]) #567

print(x[2:]) #23456789
print(x[:5]) #01234

print(s[:]) #"abcdefg" #makes a shallow copy of the sequence

Slice - third optional param is "stride" - how many characters to increment on each step

print(x[2::1]) #2345678
print(x[2::2]) #2468
print(x[2::3]) #258
print(x[2::-1]) #210 - went backward and stopped on its own
print(x[2:5:-1]) #prints empty line

Slice Assignment:

x = "0123456789"
 #slice assignment is like deleting, then inserting
s[2:4] = "123456"
print(s) #"ab123456efg"

Slice with step or stride:

s = "abcdefg"
print(s[0:5:1]) #"abcde"
print(s[0:5:2]) #"ace"
print(s[0:5:3]) #"ad"

print(s[::-1]) #"gfedcba" #reverses the sequence
print(s[4:1:-2]) #"ec"


s = "hello"
s = s.upper() #to uppercase
s = s.strip() #strip leading and trailing whitespace
s = s.lstrip()
s = s.rstrip()

"F String" or Formatted String

name = "Jane"
title = "Engineer"
text = f"name: {name} title: {title}"

Implied Line Continuation

text = f"name: {name} "
    f"title: {title}"


Number types are split into integers, floating point (fractions), and complex (imaginary).
Booleans are a subtype of integer.

Number types are created with numeric literals.

Complex numbers are in the form A+Bi where i is the imaginary number.
Python uses j instead of i, so the form is A+Bj.

x = 3+4J
y = 3+4j
z = complex(3,4)

Scientific numbers can be defined in the form nEm or nem.

x = 5E244
y = 5e244

Binary (digits 0-1):

x = 0B100
y = 0b100
z = 4

Octal (digits 0-7):

x = 0O100
y = 0o100
z = 64

Hexadecimal (digits 0-F):

x = 0X100
y = 0x100
z = 256



x = {1, 2, 3}
y = {2, 3, 4}

print(x - y) #{1}

print(x | y) #{1, 2, 3, 4} #x + y is not valid syntax

print (x & y) #{2, 3}

print(x ^ y) #{1, 4}

Add - add a value to the set
If the value is already in the set, there is no error

x = {1, 2, 3}
print(x) #{1, 2, 3, 5} #order may vary

Update - add each value in the iterable to the set

x = {1, 2, 3}
x.update([5, 6, 7, 7, 7])
print(x) #{1, 2, 3, 5, 6, 7} #order may vary

Remove - removes one value from the set
Throws a KeyError if the value is not in the set

x = {1, 2, 3}
print(x) #{1, 3}

Copy - create a new set with all the same values in it

x = {1, 2, 3}
y = x.copy()
print(y) #{1, 2, 3}


A frozenset is an immutable set. You'll need this if you want to create a set of sets; the inner sets will have to be frozensets.

a = set([1,2,3])
b = set([4,5,6])

a2 = frozenset(a)
b2 = frozenset(b)

c = set([a2, b2])


A dictionary is an unordered list of key:value pairs.
Dictionaries are mutable.

Keys must be unique within the dictionary.
Keys can be any immutable type.
The keys are used as indexes to access the values.

x = { "name":"Bob", "degree":"business", "age":23 }

print(x) #{'name': 'Bob', 'degree': 'business', 'age': 23}
print(x["name"]) #Bob

z = {} #empty dictionary


x = { "name":"Bob", "degree":"business", "age":23 }
x["age"] = 45
print(x["age"]) #45

In - returns boolean for whether a given key is in the dictionary

x = { "name":"Bob", "degree":"business", "age":23 }
if "name" in x:

Keys - returns an iterable view of the dictionary keys

x = { "name":"Bob", "degree":"business", "age":23 }
for key in x.keys():
    print(key, "=", x[key])

You cannot edit a dictionary while iterating over the key view. You have to create a copy of the iterable first.

x = { "name":"Bob", "degree":"business", "age":23 }
keys = list(x.keys())
for key in keys:
    if x[key] == 23:
        del x[key]

Values - returns an iterable view of the dictionary values

x = { "name":"Bob", "degree":"business", "age":23 }
for value in x.values():

Delete - remove a key and its value from a dictionary

x = { "name":"Bob", "degree":"business", "age":23 }
del x["degree"]
print(x) #{ "name":"Bob", "age":23 }

Pop - returns a value from a key, or a default if the key doesn't exist; also deletes the key:value

x = { "name":"Bob", "degree":"business", "age":23 }
y = x.pop("degree", None)
print(x) #{ "name":"Bob", "age":23 }
print(y) #"business"

Zip - create a dictionary from a list of keys and a list of values

    x = dict(zip(('a','b','c','d','e'),(1,2,3,4,5)))
    #equivalent to {'a':1, 'b':2, 'c':3, 'd':4, 'e':5 }


None is a singularity object, so all Nones are equal.

None is always returned from functions that don't return anything.

None's truth value is False.

You can use None anywhere it makes sense to you.

x = None


Int - convert string to integer; throws an error if conversion is not possible

x = int("45") #45

Str - convert anything to a string

x = str(359) #"359"

Ord - convert character to ascii code integer

x = ord('a')
print(x) #97

Chr - convert integer ascii code to character

x = chr(97) 
print(x) #'a'

Bin - convert integer to binary (base 2) form

x = bin(10)
print(x) #"0b1010"


Python has not maximum size on integers.

Collections have a maximum size.

import sys


import datetime

datetime_string = "Jun 28 2018 7:40AM"
datetime_parsed = datetime.datetime.strptime(datetime_string, "%b %d %Y %I:%M%p")

datetime_string = "2018-06-29 08:15:27.243860"
datetime_parsed = datetime.datetime.strptime(datetime_string, "%Y-%m-%d %H:%M:%S.%f")

print('Time:', datetime_parsed.time())
print('Date-time:', datetime_parsed)

print('My format:', datetime_parsed.strftime("%Y-%m-%d"))

%Y year in 4 digits
%m month
%d day of month
%H hour in 24 hour cycle
%M minutes
%S seconds
%f microseconds
full datetime documentation

Duck Typing

"if it looks like a duck and quacks like a duck..."

Because Python is dynamically typed, you can do this:

class Duck:
    def quack(self):
        print "Quack!"
class Goose:
    def quack(self):
        print "I am also a duck :)"
def make_some_noise(animal):
make_some_noise(Duck()); //prints Quack!
make_some_noise(Goose()); //prints I am also a duck :)
These classes are not explicitly related to each other, but can be used interchangeably as long as they implement the same method names.

Optional Type

aka Typing Hints
You can specify which type a method/function parameter expects.

Big Numbers


Python implicitly supports very large integers.

Float Or Decimal

To use very precise floating point numbers:

import decimal

Set Precision
decimal.getcontext().prec = 100
x = decimal.Decimal(y - z)

Switch is not supported in Python.

Keywords "continue" and "break" and "pass" are supported.


if a == b:
    print("a == b")
elif b == c:
    print("b == c")
    print("none of the above")

Single line (the else is required here)

x = 5 if a == b else 6 #if a == b then x = 5 otherwise x = 6


For can iterate over any sequence: string, list, tuple, set, dictionary

x = ["a", "b", "c"]
for myVar in x:
y = {"a":1, "b":2, "c":3}
for key in y:
    print(key, "=", y[key])
for v in y.values():

A for loop can have an else statement, which executes at the end of the loop (unless the loop was broken)

for x in y:

You cannot edit the for loop iterator like in javascript or C#

backedUp = False
for i in range(5):
    if not backedUp:
        i -= 1
        backedUp = True
 #outputs 0 1 2 3 4

break: exit the loop, skipping the else statement

continue: skip ahead to the next iteration of the loop


Continues looping until the condition is False.

while x < 10:
    x += 1

While can also have an else that is executed once at the end, if loop is not broken.

while x < 10:

break: exit the loop, skipping the else statement

continue: skip ahead to the next iteration of the loop


Enumerate adds a built-in counter or index or row number to a loop.
The enumerate actually returns tuples of (index, value), which are being mapped to our two variables.

for index, value in enumerate(myCollection):
    #do stuff

Enumerator starts count at zero by default, but you can specify the starting index.

for index, value in enumerate(myCollection, 5):
    #do stuff


A comprehension is a one-line expression defining a sequence of values that allows you to filter and edit the results.

x = [0 for i in range(10)] #[0,0,0,0,0,0,0,0,0,0]

y = [i*i for i in range(10)] #[0,1,4,9,16,25,36,49,64,81]

z = [str(i) for i in range(10) if i % 2 == 0] #["0","2","4","6","8"]


Operator: arithmetic and logical symbols
Operand: the values the operator uses

    + plus
    - minus
    * times
    / divide
    // divide then floor
    % modulus (remainder of divide then floor)
    ** power
    abs(x) absolute value
    divmod(x, y) returns tuple (x/y, x%y)

Python does not support ++ or --

You can separate arithmetic phrases with parentheses ( )

    and (also written &)
    or (also written |)
You can separate logical phrases with parentheses ( )
    & and
    | or
    ^ xor (when only one is true)
    ~ not
    << shift left
    >> shift right

Built-In Functions


abs(a) #absolute value

divmod(a, b)

min(a, b, c...)

max(a, b, c...)

pow(a, b) #a to the power b

round(a) #rounds a float to an int

sum(a, b, c...)


Range returns a sequence of consecutive integers.
Sequence-type "range".

Common usage

for myVar in range(5):
    print(myVar) #0 1 2 3 4

Return range from 0 to N-1

x = range(5) #basically 0, 1, 2, 3, 4
print(type(x)) #<class 'range'>
print(x) #range(0, 5)

Returns a range from N to M-1

x = range(2, 5) #basically 2, 3, 4

Returns a range from N to M-1, with strides of P

x = range(2, 20, 3) #basically 2, 5, 8, 11, 14, 17

Convert a range to a list

x = list(range(10)) #[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Proving that ranges are determined once, at the beginning of the loop

s = "a"
for i in range(0, len(s)):
    s += "a"
    if len(s) > 10:
 #results in "aa"


Join - join the elements of a collection into one string
The syntax begins with the string delimiter to be used

x = ["a", "b", "c"]
y = "_".join(x)
print(y) #a_b_c

Split - divide a string into a list of strings, based on a delimiter

x = "a,b,cde"
y = x.split(",") #["a", "b", "cde"]

text = "Bob,business,45"
name,degree,age = text.split(",")

Strip - remove leading and trailing white spaces and end-line characters

x = " text ".strip()
print(x) #"text"

IsDigit - returns boolean - could this string be converted to am integer?

if myString.isdigit():
    x = int(myString)

EndsWith - returns boolean - does string end with substring?

if "hello".endswith("llo"):


Append - add an object to the end of the list

x = [1, 2, 3]
print(x) #[1, 2, 3, 4]

x.append([5, 6])
print(x) #[1, 2, 3, 4, [5, 6]]

Extend - add each element in the iterable to the end of the list

x = [1, 2, 3]
x.extend([4, 5, 6])
print(x) #[1, 2, 3, 4, 5, 6]

Insert - insert an element into a position in the list

x = ["a", "e", "i"]
x.insert(1, "b") #insert "b" at index position 1
print(x) #["a", "b", "e", "i"]

Index - returns the index of the first instance of this value in the list
Throws an error if the value is not in the list

x = ["a", "b", "c"]
y = x.index("c")
print(y) #2

Delete - remove an element by index

x = ["a", "b", "c"]
del x[0]
print(x) #["b", "c"]

Remove - remove the first instance of this value from the list

x = ["a", "b", "c", "b"]
print(x) #["a", "c", "b"]

Clear - remove all elements from the list

x = ["a", "b", "c"]
print(x) #[]
Basic ascending sort

x = [5, 3, 7, 4, 1]
x.sort() #only works on lists
print(x) #[1, 3, 4, 5, 7]

Basic ascending sort for any iterable

a = { 1:"one", 2:"two", 3:"three", 6:"six", 7:"seven", 9:"nine" }
x = [6,3,7,9,3,2]
y = sorted(x)
print(y) #[2, 3, 3, 6, 7, 9]

z = sorted(x, key=lambda s:a[s]) #can specify sort key
print(z) #[9, 1, 3, 3, 2, 7, 6]


Return a list of string-names of all the methods and properties of an object

x = dir(myObject)



The write command does not automatically add an end-line character.

Create or overwrite file

f = open("filename.txt", "w") #w for write

Append to file

f = open("filename.txt", "a") #a for append
f.write("more text")

Append or create

f = open("filename.txt", "a+")


Read - read the entire file as one string

f = open("filename.txt", "r") #r for read
text = #read will read in the entire file at once as a string

Read(charCount) - read a few characters at a time

f = open("filename.txt", "r") #r for read
charCount = 1
next = #read a few chars at a time
while next != "":
    next =

Readline - read one line at a time
The line still has the end-line character

f = open("filename.txt", "r") #r for read
next = f.readline() #reads one line (to \n char)
while next != "":
    next = f.readline()

Readlines - read in all lines at once, as a list of strings
Each line still has the end-line character

f = open("filename.txt", "r") #r for read
text = f.readlines()

Byte Position

Tell - returns the current byte position of the cursor in the file

currentBytePosition = file.tell()

Seek(position) - move to an absolute byte position in the file, 0)

Seek(position, 1) - move forward the specified number of bytes, 1)

Seek(position, 2) - move to the specified byte position, counting backward from the end of the file, 2)


import os.path
from os import path

Path or file exists

x = path.exists('path/fileName.txt')

Path is a directory

x = path.isdir('path')

Path is a file

x = path.isfile('path/fileName.txt')

Make a web request

import requests

response = requests.get("")
if(response.status_code != 200):
json = response.json() #data converted to dictionary
for person in json['people']:

Functions must be defined before they are called (higher in the text file).
Functions that are in an imported file can call each other in any order.

def funcName(): #definition
    print("my function")
funcName() #invocation

Empty function:

def empty():

First Class

Functions are first class objects in Python.

They can be passed as arguments.
They can be returned from functions.

They can be nested within other functions.
Nested functions are only in-scope to their direct parent.
Nested functions have access to parent variables.


Variables assigned a value anywhere in the function are assumed local, unless explicitly declared global.

x = 1

def funcA():
    global x
    x = 2
    print(x) #prints 2
print(x) #prints 2

Declaring a new variable global within a function will define it as a global variable, but you should not rely on this because the variable does not exist until the function is run

def funcA():
    global x
    x = 2
print(x) #throws an error if funcA is not run first

Method Overloading

Python does not support method overloading.

This example won't work, because the last function definition overwrites the earlier ones of the same name.

 #!!! This won't work !!!
def myFunc(a):
def myFunc(a, b):
    print("a b")
def myFunc(a, b, c):
    print("a b c")
myFunc(1, 2)
myFunc(1, 2, 3)

Monkey Patching

Monkey patching is overwriting the behavior of a function or object after it is defined. Do not do this.

Default Parameters

You can use default parameter values to get close to method overloading.

def myFunc(a, b=None, c=None):
    if c == None:
        if b == None:
        print(a, b)
    print(a, b, c)
myFunc(1, 2)
myFunc(1, 2, 3)

Passing Parameters

Python passes all arguments as pass-by-object-reference. That means that all object operations used in a function will affect the original argument, except for instantiation.

Immutable objects are de-facto pass-by-value because any edit operation automatically re-instantiates the object.

def funcA(listA):
    listA.append("a") #most object operations affect the original argument
def funcB(listB):
    listB = ["b", "b", "b"] #instantiate does not affect the original argument

x = [1, 2, 3]
print(x) #[1, 2, 3]

print(x) #[1, 2, 3, "a"]

print(x) #[1, 2, 3, "a"]

Strict Signatures

Use strict signatures to enforce usage standards for how to pass arguments to methods. This can protect callers from breaking changes as the argument list of a function changes over time.
- For instance, any parameters that are likely to be removed later, or to be reordered for the legibility of the function signature, can be marked as "keyword passing only" so that calling code is not broken by changes to the function.
- "positional passing only" parameters should be completely solid arguments that are central to the use case of the function, and the function call should be legible without seeing their argument names.
- Can ensure all calls to the function are formatted consistently.

Force all callers to use positional passing for the parameters left of the "/"

def funcA(paramA, paramB, /, paramC, paramD):
 # ways to all the function    
funcA("a", "b", "c", "d")
funcA("a", "b", "c", paramD="d")
funcA("a", "b", paramC="c", paramD="d")

Force all callers to use keyword passing for the parameters right of the "*"

def funcA(paramA, paramB, *, paramC, paramD):
 # ways to all the function    
funcA("a", "b", paramC="c", paramD="d")
funcA("a", paramB="b", paramC="c", paramD="d")
funcA(paramA="a", paramB="b", paramC="c", paramD="d")

Doing both at once

def funcA(paramA, paramB, /, *, paramC, paramD):

 # ways to all the function    
funcA("a", "b", paramC="c", paramD="d")

*args **kwargs

args is a tuple containing an undefined number of function arguments.

kwargs is a dictionary containing an undefined number of named (aka keyword) arguments.

def myMethod(a, b, *args, **kwargs):

myMethod(1, 2, 3, 4, 5, name="Bob", age=6)
    #(3, 4, 5)

"args" and "kwargs" are conventions. You can call these parameters anything you want, provided they start with "*" and "**".

You cannot list normal parameters between the *args and *kwargs.

You cannot enter keyword arguments into the *args tuple, and you cannot enter normal arguments into the **kwargs dictionary.


aka Inner Functions

Functions can be defined within in functions in Python, like in Javascript.

The inner function has access to the outer function's local variables and parameters, even after the outer function has completed.
The inner function is not defined until the outer function is run.

See Javascript notes about closures.


Lambdas are single-expression, anonymous functions. They do not use a "return" keyword, they simply return the result of their single expression, if any.

myDisplay = lambda: print("hey")
myDisplay() #prints "hey"

myCalc = lambda: 5 + 3
print(myCalc()) #prints 8

Lambdas with parameters:

myCalc = lambda x: x + 3
print(myCalc(6)) #prints 9

myCalc = lambda x, y, z: x + y + z
print(myCalc(3, 4, 5)) #prints 12

Type Hinting

Python 3.5 and higher.

Specify what parameter types are expected, and what type will be returned.

The types of parameters passed in are checked at runtime; sending in the wrong type will result in a TypeError exception.
Subtypes of the specified type are accepted.

The return type is not verified, so you can actually return any value you want.

    #expects a string parameter, and returns a string
    def hello_world(name: str) -> str:
        return(f"Hello {name}")

Python 3.5.2 and higher: type hinting for function signatures.

    from typing import Callable

    #parameter is expected to be a function that accepts two integers as arguments and returns a string
    def method_a(callback: Callable[[int,int],str]) -> None:

    #parameter is expected to be a function (with any arguments) that returns a string
    def method_b(callback: Callable[...,str]) -> None:
        print(callback([], "a", 1)

When any type is acceptable

from typing import Any

def myFunction(param: Any):

You can install (with pip) and use mypy to verify a python file is being consistent to the type hints.

Type Aliasing

Python 3.5 and higher. Related to type hinting.

You can define an alias for a type, and then use the alias anywhere you'd use the type.

    MyAlias = str

    def hello_world(name: MyAlias) -> MyAlias:
        return(f"Hello {name}")

    print(hello_world("Bob")) #outputs "Hello Bob"
    print(hello_world(1))     #throw TypeError

    Example from documentation:

    from typing import List, Dict, Tuple, Sequence

    Vector = List[float]
    ConnectionOptions = Dict[str,str]
    Address = Tuple[str,int]

    #these alias can be used to enforce parameter types

New Type

Python 3.5 and higher. (Maybe belongs in a different section of notes)

Easily define a new subtype of a type, with a distinct name.

from typing import NewType

UserId = NewType('UserId', int)

Note that using UserId in an operation will return an int.


Generator functions are used to gradually access a large data set without loading the whole thing into memory at one time.
Or to generator an infinite sequence.
Or for a function that needs to maintain an internal state, but it is so small you don't want to create a class for it.

Generator functions return a Lazy Iterator.
A lazy iterator is an object you can loop over with the list syntax.

Generator Function to read a large file

def file_reader(file_name):
    for row in open(file_name, "r"):
        yield row
reader = file_reader("name.txt")
for line in reader:

Generator Comprehension or Generator Expression

reader = (row for row in open("name.txt"))
Note that list comprehensions are surrounded by [] while generator comprehensions are surrounded by ().
A list comprehension can be faster to evaluate, assuming the result fits in memory.

generating an infinite sequence

def infinite():
    n = 0
    while True:
        yield n #the function will pause here until you call for the next item
        n += 1

infinite_generator = infinite()
print(next(infinite_generator)) #0
print(next(infinite_generator)) #1
print(next(infinite_generator)) #2

If you try to get a next value past the end of the generator's iteration, you'll get a StopIteration error.

Coroutine: You can send a value back to the generator

def gen():
    n = 0
    while True:
        n = (yield n)
        n += 1

g = gen()
print(next(g)) #0
print(next(g)) #6

Tell a generator to stop

g = gen()
for i in gen:
    if i > 100:

There also a .throw() command.

Example of using generators to build a data pipeline
Given a very large csv file:

line_gen = (line for line in open("big_file.csv"))
column_gen = (line.rstrip().split(",") for line in line_gen)
column_names = next(column_gen) #first line is the column names
data_dict_gen = (dict(zip(column_names, fields)) for fields in column_gen) #create a dict for one line of data using the column names as keys

 #if you want the dollar amount raised in round "a"
funding = (
    for data in data_dict_gen
    if data["round"] == "a"
funding_total = sum(funding)

Function Decorators

A decorator function takes in another function and can run code before and after running the passed-in function.
So a decorator is a function wrapper.

Use cases: logging, timers, metrics, try-again loops, rate limiting, authorization, caching, ...


def i_am_wrapper(func):
    def inner():
    return inner

def i_am_wrapped():

i_am_wrapper(i_am_wrapped)() #outputs the three statements
The same thing with the decorator syntax

def i_am_wrapper(func):
    def inner():
    return inner

def i_am_wrapped():

i_am_wrapped() #outputs the three statements

Nested Decorators
You can put more than one decorator on a function, they will nest

def wrapper_a(func):
    def inner():
        print('start A')
        print('end A')
    return inner

def wrapper_b(func):
    def inner():
        print('start B')
        print('end B')
    return inner

def i_am_wrapped():

i_am_wrapped() #outputs start A, start B, wrapped, end B, end A

To pass through a variable number of parameters:

def wrapper_a(func):
    def inner(*args, **kwargs):
        print('start A')
        func(*args, **kwargs)
        print('end A')
    return inner

def i_am_wrapped(text):
    print('wrapped: ' + text)

To pass through a returned value:

def wrapper_a(func):
    def inner(*args, **kwargs):
        print('start A')
        result = func(*args, **kwargs)
        print('end A')
        return result
    return inner

def i_am_wrapped(text):
    print('wrapped: ' + text)
    return "done"

A decorated function is actually set to the wrapper function that is returned from the decorator.
So looking at __name__ on the function will not give the data you expect.
To provide the expected data:

import functools

def wrapper_a(func):
    def inner(*args, **kwargs):
        return = func(*args, **kwargs)
    return inner

Decorators with arguments - requires another layer of function

def repeat(times):
    def repeat_func(func):
        def inner():
            for _ in range(times):
        return inner
    return repeat_func

def hello():
(Ugh, why isn't the syntax just "repeat(func, times)"?)

Decorators with optional arguments

def repeat(_func=None, *, times=1):
    def decorator(func):
        def inner():
            for _ in range(times):
        return inner
    if _func is None:
        return decorator
    return decorator(_func)

def hello():

def goodbye():

Example: debugging help

import functools

def debug(func):
    """Print the function signature and return value"""
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={repr(v)}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__}() returned {repr(value)}")
        return value
    return wrapper_debug

Example: register a function and then return it as-is

PLUGINS = dict()

def register(func):
    """Register a function as a plug-in"""
    PLUGINS[func.__name__] = func
    return func

Because this is creating Closures, you can maintain state within the decorator function.
Ex: keep a count of how many times the decorated function has been called.


Objects are instantiated from Classes.
Objects use internal Dictionaries to store their data.

class MyEmpty:

class MyClass:
    def __init__(self, inputA):
        self.varA = inputA
        print("object initialization")
    def funcA(self):

x = MyClass(5) #"object initialization"
print(x.varA) #5
x.funcA() #"hey"

Dunder Methods

Dunder Methods aka Special Methods aka Magic Methods
the build in methods that start and end with double underscore (__)
such as __init__

Dunder Methods are meant to be invoked implicitly, not explicitly.

By convention, methods and variables that start with a double underscore (__) should be treated as private.

Built-in method __new__ is always run before __init__. It allocates memory and passes the object to __init__(self).

class MyClass:
    # this is called when you run "print(myClassInstance)" or "str(myClassInstance)"
    def __str__(self):
        return "string"
    # this is like __str__ but intended for developers only instead of something end users see
    # this is called when you run just the variable name in the Interactive Shell
    def __repr__(self):
        return "string"
        # example: for a date, returning ", 11, 6)" which tells you how to instantiate the object
        # it is convention to return a value that can be passed to "eval()" and result in a new object

+ __add__
* __mul__
- __sub__
% __mod__
/ __truediv__
// __floordiv__
** __pow__(self, power)
abs(x) __abs__
-x __neg__
+x __pos__
< __lt__
<= __le__
== __eq__
!= __ne__
> __gt__
>= __ge__
<< __lshift__
>> __rshift__
and __and__
or __or__
xor __xor__
not __invert__
[index] __getitem__(self, index)
in __contains__(self, value)
len __len__
str __str__
int(x) __int__
long(x) __long__
float(x) __float__
complex(x) __complex__
divmod(x, y) __divmod__


All classes also have available these attributes:

print(x.__class__) #outputs <class '__main__.MyClass'>
print(a.__dict__) #outputs {'varA': 5}

Instance attributes
- no privacy option
- naming convention is __var__ to indicate the variable should be treated as private

class MyClass:
    def __init__(self):
        self.instance_variable = 3;

    def print(self):

x = MyClass()
x.print() #"3"
print(x.instance_variable) #"3"
print(hasattr(x, "instance_variable")) #"True"

Class attributes (called Static variables in C#)
- no privacy option
- value can change
- editing a class variable affects all instances of the class
- class_var and self.class_var are different variables

class MyClass:
    class_variable = 4;

    def __init__(self):
        self.class_variable = 3;
    def print(self):
        print(MyClass.class_variable, self.class_variable)

x = MyClass()
x.print() #"4 3"
MyClass.class_variable = 5
x.print() #"5 3"

Attribute Lookup Chain: an object instance will search its own __dict__ for an attribute, then it will look in its class' __dict__

class MyClass:
    x = 4;

    def __init__(self):
        self.y = 3;

myInstance = MyClass()
print(MyClass.x) #4
print(myInstance.x) #4
This is actually how methods are found, as well. The class instance is searched first, then the class is searched.

You can assign class attributes within the class, and outside the class.
You can access all attributes of a class instance.

class A(object):

a = A()
a.x = 10
a.y = "string"
print(a.__dict__) #outputs {'x': 10, 'y':'string'}

Listing all the public attributes of a class (not an instance of a class):

import random
cls = "random" # name of the class as a string
all_attributes = [x for x in dir(eval(cls)) if not x.startswith("__") and callable(eval(cls + "." + x))]

Python does not have access modifiers Public or Private, all attributes are public.
By convention, developers should treat attributes that start with an underscore as private (_example).

To mimic encapsulation (which python does not actually enforce), write your own getters and setters for each "private" attribute.
This is as close as python gets to supporting Properties.

class Person:
    def __init__(self, name, birthDate):
        self._name = name
        self._birthDate = birthDate
    def get_name(self):
        return self._name
    def set_name(self, name):
        self._name = name
    def get_age(self):
        return calculate_age(self._birthDate)
This is considered sufficient protection within Python programming.

Name Mangling: put two underscores at the start of an attribute name, and Python will raise a runtime error if the attribute is accessed from outside the class.

class Person:
    def __init__(self, birthDate):
        self.__birthDate = birthDate
    def get_age(self):
        return calculate_age(self.__birthDate)

person = Person('1990-01-01')
print(person.__birthDate) #raises an AttributeError
This is because the actual attribute name, when it is declared with two underscores, is automatically set to "_Person__birthDate" (prefixed with an underscore and the class name).
This altered name IS still publicly available, once you know what it is.

person = Person('1990-01-01')
print(person._Person__birthDate) #prints the birth date


Properties is one way to do Managed Attributes

"@property" is a "class decorator" using the "descriptor protocol"

class Person:
    def __init__(self, name): = name #this uses the property name setter
    # this is the getter
    def name(self):
        return self._name

    # this is the setter
    def name(self, name):
        self._name = name

    # this is the deleter (less common)
    def name(self, name):
        del self._name
person = Person('bob')
print( #properties can be accessed without parentheses

This allows you to update an attribute that started out public ( was directly accessing the attribute) and is now being made "private" (updated to person._name). The change will be backwards compatible, provided the property returns the same data type as the original attribute.

Note that the name of the property does not have to correlate to the name of the "private" attribute. There is no automatic magic linking them based on their names. It is, of course, best practice to give them correlated names.

In order to make a read-only property, simply do not implement the setter.
In order to make a write-only property, you must still implement the getter and setter. In the getter, raise an error.

Computed properties are properties that return a computed value, such as returning "annual salary" based on attribute "hourly wage".


Slots are used to decrease memory allocation and to increase data access speeds.

This also results in users not being able to add more attributes to an instance. All possible attributes must be listed when "slots" is declared.
With slots, you define the full list of attributes for the object once, and no new attributes can be added by anyone.

class A(object):
    __slots__ = ['x'] #this line here

    def __init__(self, x):
        self.x = x
a = A(10)
a.x = 20
a.y = "string" #AttributeError: 'A' object has no attribute 'y'

Attribute __dict__ is not available when you use slots. Print __slots__ instead to inspect the attributes.
If you define __dict__ manually, it will not be automatically filled.

Slots with inheritance:

class Person:
    __slots__ = ("name", "birthDate")
    def __init__(self, name, birthDate): = name
        self.birthDate = birthDate

class Employee(Person):
    __slots__ = ("hiredDate") #this adds another attribute to theh inherited attributes
    def __init__(self, name, birthDate, hiredDate):
        super().__init__(name, birthDate)
        self.hiredDate = hiredDate

Method Types

Note that methods in a class are technically attributes. You can see them listed in the __dict__ of the class.
They are of type "function".
Example: you can run a method with "MyClass.__dict__['my_method'](myClassInstance, parameter)"

(In Python 3)

Instance methods can modify the current instance, through "self".
Instance methods can modify the current class, through "self.__class__".

Class methods can modify class state, affecting all instances of the class, through "cls".

Static methods cannot modify the class nor instance state. They are primarily used to namespace a method.
Static methods are pretty much defunct since Class methods were added to Python.

class MyClass:
    #this is an Instance Method or Instance Function
    def methodA(self):
        return ("instance method called", self)
    def methodB(cls):
        return ("class method called", cls)
    def methodC():
        return "static method called"
x = MyClass()

MyClass.methodA(x) #x.methodA() really means this



Note that calling the default parameters "self" and "cls" is a convention, you an call them anything, but stick with the convention.

Example of using classmethod for the factory pattern

import datetime
import date
class Person:
    def __init__(self, name, age): = name
        self.age = age
    def fromBirthYear(cls, name, birthYear):
        return cls(name, - birthYear)
x = Person("Bob", 49)
y = Person.fromBirthYear("Jane", 1978)

Factory Function aka Alternative Constructor: a class or static method that builds and returns a new instance of the class

class MyClass:
    def builder(cls, param):
        return cls(param, constant) #use "cls" to instantiate a new instance instead of hardcoding the class name here


Classes can inherit from multiple classes.

Method overriding: subclass methods override superclass methods of the same name.

By default, all classes implicitly inherit from "object" class.

Subclass inherits from Superclass OR
Derived Class inherits from Base Class OR
Child class inherits from Parent Class

Basic inheritance:

class Person:
    def __init__(self, firstName, lastName):
        self.firstName = firstName
        self.lastName = lastName
    def GetName(self):
        return self.firstName + " " + self.lastName
class Employee(Person):
    def __init__(self, firstName, lastName, id):
        Person.__init__(self, firstName, lastName) #call base constructor = id

Inspecting class relationships with built-in methods:

bob = Person('bob', 'smith')
if isinstance(bob, Person):
    print("bob is an instance of the Person class")
if issubclass(Employee, Person):
    print("Employee class is a subclass of the Person class")

You can access superclass methods from the subclass:

class Employee:
    def __init__(self, salary):
        self.salary = salary;
    def get_a_raise(self, percent):
        self.salary += self.salary * (percent/100)

class BonusEmployee(Employee):
    def get_a_raise(self, percent, bonus):
        super().get_a_raise(percent) #runs the superclass method first
        self.salary += bonus         #then runs specialized code
Calling "super().get_a_raise(percent)" here using MRO to determine what method to call. (This is preferred.)
Calling "Employee.get_a_raise(self, percent)" instead would lock in which inherited method to call.

Multiple Inheritance is supported in python.

Diamond inheritance: the first class inherited from takes priority over later classes in the inheritance list.

class A:
    def id(self):

class B(A):
    def id(self):
class C(A):
    def id(self):

class D(B,C):

class E(C,B):

d = D();; #outputs B

e = E();; # outputs C

Diamond inheritance in Python 3:
- a subclass that overrides a superclass method takes priority over a subclass listed earlier in the inheritance list that does not override the superclass method.
- The "Method Resolution Order" (MRO) is depth-first (later generation classes first) then left-to-right as the classes are ordered in the inheritance list. (MRO is also called C3 Superclass Linearisation.)

You can view the MRO of a particular class:


MRO example: takes priority even when B is earlier in the inheritance list 
 #because it is closer to D and E in the inheritance tree than A is.
class A:
    def id(self):

class B(A):
class C(A):
    def id(self):

class D(B,C):

class E(C,B):

d = D();; #outputs C

e = E();; #outputs C

This demonstrates that depth is determined as which superclass is closest to the calling class, rather than which superclass is furthest from the top-level/origin class.

class A:
    def id(self):

class B(A):
    def id(self):
class C(A):
    def id(self):

class D(C):
    def id(self):

class E(B,D):
class F(D,B):
 #inheritance tree:
 # A <= B <= E,F
 # A <= C <= D <= E,F

e = E();; #outputs B

f = F();; #outputs D

When calling base methods, there are two ways to do it in Python 3:

Person.__init__(self, firstName, lastName)
super().__init__(firstName, lastName) #super() refers the the MRO superclass

class A:
    def id(self):

class B(A):
class C(A):
    def id(self):

class D(B,C):
    def id(self):

d = D();; # outputs D C A A

Base vs Bases

print(type(myInstance).__base__.__dict__["attr"]) #goes to base/super class, the first one in the __bases__ list

print(type(myInstance).__bases__) #full list of base classes given in subclass definition


Composition refers to instances of objects that set attributes equal to other instances of objects.
Inheritance makes an "is a" relationship.
Composition makes a "has a" relationship.

class Schedule:
    def __init__(self, wakeUpTime, sleepTime):
        self.wakeUpTime = wakeUpTime
        self.sleepTime = sleepTime

class Person:
    def __init__(self, birthDate, schedule):
        self.birthDate = birthDate
        self.schedule = schedule #this is the composition part
schedule = Schedule(8, 10)
person = Person('2000-1-1', schedule)

Mixin Class

Mixin classes are not enforced by python, they are a naming convention.
Adding the "Mixin" suffix to a class name indicates that the class is intended to be used in multiple inheritance to add a little boilerplate functionality to many classes.
Example: to adding logging methods, or reflection (self-inspection) methods, or some other set of utilities.

class ReflectionMixin:
    #methods here for commonly used reflection utilities


An instance of a metaclass is a class.
All metaclasses must inherit from "type" instead of "object".

required = True

class MyMetaClass(type):
    def __init__(cls, clsname, superclasses, attributedict):
        if required:
            cls.myAttribute = "Required"
            cls.myAttribute = "Optional"

class A(metaclass=MyMetaClass):
required = False

class B(metaclass=MyMetaClass):
a = A()
print(a.myAttribute) #outputs Required

b = B()
print(b.myAttribute) #outputs Optional
In this example, A and B inherit from different instances of MyMetaClass. The behavior of the instances they inherit from was changed by the state of the program.

You can use a metaclass to create a Singleton object:

class Singleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]
class SingletonClass(metaclass=Singleton):

class RegularClass():

x = SingletonClass()
y = SingletonClass()
print(x == y) #outputs True

x = RegularClass()
y = RegularClass()
print(x == y) #outputs False
But since you can do the same thing with normal classes, why use metaclasses?

class Singleton(object):
    _instance = None
    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = object.__new__(cls, *args, **kwargs)
        return cls._instance
class SingletonClass(Singleton):

class RegularClass():

x = SingletonClass()
y = SingletonClass()
print(x == y) #outputs True

x = RegularClass()
y = RegularClass()
print(x == y) #outputs False

Using metaclass to automatically apply a decorator to every method in each subclass:

class FuncCallCounter(type):
    #this metaclass will decorate all the methods of the subclass with "call_counter"
    def call_counter(func):
        # decorator for counting the number of function/method calls
        def helper(*args, **kwargs):
            helper.calls += 1
            return func(*args, **kwargs)
        helper.calls = 0
        helper.__name__= func.__name__
        return helper
    def __new__(cls, clsname, superclasses, attributedict):
        for attr in attributedict:
            if callable(attributedict[attr]) and not attr.startswith("__"):
                attributedict[attr] = cls.call_counter(attributedict[attr]) #applying decorator to subclass method
        return type.__new__(cls, clsname, superclasses, attributedict)
class A(metaclass=FuncCallCounter):
    def B(self):
    def C(self):

x = A()
print(x.B.calls, x.C.calls) #outputs 0 0

print(x.B.calls, x.C.calls) #outputs 1 0

print(x.B.calls, x.C.calls) #outputs 2 1

Abstract Class

Python does not automatically support abstract class, but it does provide a module that supports it.
Module "abc" stands for "abstract base class"

from abc import ABC, abstractmethod
class MyAbstractClass(ABC): #must inherit from ABC
    def __init__(self, value):
        self.value = value
        super().__init__() #not required
    @abstractmethod #must contain at least one abstractmethod
    def do_something(self):
class A(MyAbstractClass):

    def do_something(self):
        print("do something")
a = A(10)
a.do_something() #outputs do something

b = MyAbstractClass(10) #TypeError: Can't instantiate abstract class MyAbstractClass with abstract methods do_something

A class that inherits from ABC but does not contain any abstractmethods is not treated as abstract.

Abstract classes are allowed to contain non-abstractmethods. Inheritance of these methods works like normal inheritance.

A class that inherits from an abstract class must override all abstractmethods of that class. Or it must inherit all those methods from elsewhere before inheriting from the abstract class.

from abc import ABC, abstractmethod
class MyAbstractClass(ABC):
    def __init__(self, value):
        self.value = value
    def do_something(self):
class A():
    def do_something(self):
        print("do something")
class B(A, MyAbstractClass):
b = B(10)
b.do_something() #outputs do something

Abstract methods may contain default behavior. The inheriting subclass must still override the abstract method, but can call the default behavior:

from abc import ABC, abstractmethod
class MyAbstractClass(ABC):
    def __init__(self, value):
        self.value = value
    def do_something(self):
        print("abstract do something")
class A(MyAbstractClass):
    def do_something(self):
a = A(10)
a.do_something() #outputs abstract do something

Data Class

When a class is just a data container, where the __init__ method just sets attributes equal to parameters of the same name, and the attributes are all public, then you could make a Data Class instead.

from dataclasses import dataclass

class MyClass:
    myString: str #btw this is a Type Hint
    myInt: int
    myOtherString: str
This will define a class that expects 3 parameters on initialization, and sets them to these attributes.

You can still define methods in data classes

from dataclasses import dataclass

class MyClass:
    myString: str
    def myMethod(self):
        return 0

Python 3.10+
you tell that data class you want slots like this

from dataclasses import dataclass

class MyClass:
    myString: str


A descriptor is a Python object that implements at least one of the methods in the descriptor protocol.
Descriptors run special behavior (binding behavior) when they are accessed as attributes of another object.

Descriptors are instantiated just once per class. (All instances of the class share one instance of the descriptor)

Uses for descriptors: lazy loading, logging, validation, code reuse. But can be very illegible, code maintainers would need to be well versed in the system's architecture, because you are inventing your own syntax sugar / magic.

Descriptor Protocol

__get__(self, obj, type=None) -> object
__set__(self, obj, value) -> None
__delete__(self, obj) -> None
__set_name__(self, owner, name) #added in Python 3.6
A non-data descriptor implements just __get__.
A data descriptor implements __set__ or __delete__. Data descriptors have precedence during the lookup process.
Here, "self" is the instance of the descriptor object.
Here, "obj" is the instance of the object the descriptor is attached to.
Here, "type" is the type of the object the descriptor is attached to.
Here, "name" is the name of the attribute that this descriptor was assigned as instantiation.

A bit of a convoluted example for pre-python 3.6:

class MyDescriptor():
    def __init__(self, name): = name #this would be handled in __set_name__ once that method was added in Python 3.6

    def __get__(self, obj, type=None) -> object:
        return obj.__dict__.get( or 0 #set instance attribute

    def __set__(self, obj, value) -> None:
        obj.__dict__[] = value #get instance attribute

class MyClass():
    number = MyDescriptor("number") #set class attribute
myInstance = MyClass() #stores the descriptor in type(myInstance).__dict__["number"]
myInstance.number = 5 #stores "5" in myInstance.__dict__["number"]
print(myInstance.number) #prints "5"


class Verbose_attribute():
    def __get__(self, obj, type=None) -> object:
        print("accessing get")
        return 42
    def __set__(self, obj, value) -> None:
        print("accessing set")
        raise AttributeError("Cannot change the value")

class Foo():
    attribute1 = Verbose_attribute() #descriptor will be accessed as an attribute of another object

my_foo_object = Foo()
x = my_foo_object.attribute1 #prints "accessing get"
print(x) #prints "42"

Example of descriptor set with decorator, also an example of lazy loading taking advantage of the attribute lookup chain:

import time

class LazyProperty:
    def __init__(self, function):
        self.function = function = function.__name__

    #just "get" is defined, so this is a non-data descriptor, which shows up later in the attribute lookup chain
    def __get__(self, obj, type=None) -> object:
        obj.__dict__[] = self.function(obj)
        return obj.__dict__[]

class DeepThought:
    def meaning_of_life(self):
        return 42

my_instance = DeepThought()
print(my_instance.__dict__) #at this point, the instance has no __dict__ values
print(my_instance.meaning_of_life) #this runs class's meaning_of_life method and stores the result in the instance's __dict__
print(my_instance.__dict__) #now the instance has __dict__["meaning_of_life"] set
print(my_instance.meaning_of_life) #lookup chain finds instance.__dict["meaning_of_life"] and does not progress to class's __dict__
Result: the slow "meaning_of_life" method is only run once per instance of DeepThought.

Example of descriptor for code reuse and ?possibly? memory allocation reduction

class UpperCaseProperty:
    def __set_name__(self, owner, name): = name
    def __get__(self, obj, type=None) -> object:
        return obj.__dict__.get( or ""
    def __set__(self, obj, value) -> None:
        obj.__dict__[] = value.upper()
class Person:
    last_name = UpperCaseProperty()
    def __init__(self, first):
        self.first_name = first
p = Person('bob')
p.last_name = 'smith'
p2 = Person('jane')
p2.last_name = 'doe'
print(p.first_name) #bob
print(p.last_name) #SMITH
print(p2.first_name) #jane
print(p2.last_name) #DOE
It certainly feels clunky to need to define the special properties partly at the class level, and only set them implicitly at the instance level. This is not very legible.

Example: directory size utility

import os

class DirectorySize:
    def __get__(self, obj, objtype=None):
        return len(os.listdir(obj.dirname))

class Directory:
    size = DirectorySize()
    def __init__(self, dirname):
        self.dirname = dirname

s = Directory('songs')
g = Directory('games')
print(s.size) #outputs size of "songs" directory
print(g.size) #outputs size of "games" directory

Properties are an example of descriptors.
Properties are syntactic sugar.

With properties
class Foo():
    def attribute1(self) -> object:
        print("accessing get")
        return 42

    def attribute1(self, value) -> None:
        print("accessing set")
        raise AttributeError("Cannot change the value")

With plain descriptors
class Foo():
    def getter(self) -> object:
        print("accessing get")
        return 42

    def setter(self, value) -> None:
        print("accessing set")
        raise AttributeError("Cannot change the value")

    attribute1 = property(getter, setter)
    #full property signature
    #property(fget=None, fset=None, fdel=None, doc=None) -> object
    #property returns an object that implements the descriptor protocol

Methods are also syntactic sugar over functions/attributes/descriptors.
obj.method(*args) can be called as method(obj, *args) because of a _get_ descriptor. Methods are non-data descriptors.

import types

class Function(object):
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return types.MethodType(self, obj)

Attribute Lookup Chain

(Methods are also attributes of objects)

The full attribute lookup chain for attribute "Name":
- the result returned from the __get__ method of the data descriptor named Name
- the value of your object’s __dict__["Name"]
- the result returned from the __get__ method of the non-data descriptor named Name
- the value of your object type’s __dict__["Name"]
- the value of your object parent type’s __dict__["Name"]
- then the previous step is repeated for all the parent’s types in the method resolution order of your object
- then an AttributeError exception.

Class Decorators

Very similar to Function Decorators; the decorator function will receive a class object instead of a function object.

Decorating a class does not decorate its methods.


class Counter:
    def __init__(self, start=0):
        self.count = start
    def __call__(self): #executed when you "call" an instance of the class
        self.count += 1
        print(f"Current count is {self.count}")
x = Counter()
x() #outputs "Current count is 1"
x() #outputs "Current count is 2"

Example of a class decorator being applied to a function:

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.num_calls = 0
    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print(f"Call {self.num_calls} of {self.func.__name__}()")
        return self.func(*args, **kwargs)
def hello():


Decorators look like this:


Class Methods

See the class section for more about the @classmethod, @staticmethod, and @property decorators.

Method As Decorator

Function A can be decorated with another function B. When A is run, it is actually passed as a parameter to function B, and function B can choose when/if to run it.

def myDecorator(f):
    def wrapper():
    return wrapper
def plainFunction():
    print("plain function")
x = myDecorator(plainFunction)

 #plain function

Simplified syntax:

def myDecorator(f):
    def wrapper():
    return wrapper

def plainFunction():
    print("plain function")

 #plain function

When would you use a decorator?
To time the length of functions
To apply regular logging
To wait before executing a function - rate limiting
To require a login for a function
To validate the arguments passed into a function
Regular Expressions


Compile - compile an expression for reuse

import re
myRegEx = re.compile('ab*')

Compile has some optional flags

import re
myRegEx = re.compile('ab*', re.IGNORECASE)


Match - check if regular expression matches text
Match will only find matches at the start of the string

import re
myRegEx = re.compile('ab*')
myMatch = myRegEx.match("ggttrr")
print(myMatch) #None

myMatch = myRegEx.match("abbbcab")
print( #'abbb' #the string that matched
print(myMatch.start()) #0 #index of start of match
print(myMatch.end()) #4 #index of first char NOT in match
print(myMatch.span()) #(0, 4) #tuple of start and end indexes


Search - check if regular expression matches text
Search will find the first match anywhere in the string

import re
myRegEx = re.compile('ab*')
myMatch ="ffabbbcab")
print( #'abbb' #the string that matched
print(myMatch.start()) #2 #index of start of match
print(myMatch.end()) #6 #index of first char NOT in match
print(myMatch.span()) #(2, 6) #tuple of start and end indexes


FindAll - check if regular expression matches text
FindAll will return of list of all matches anywhere in the string

import re
myRegEx = re.compile('ab*')
myMatches = myRegEx.findall("ffabbbcab")
print(myMatches) #['abbb', 'ab'] #list of matched strings


FindIter - like FindAll but you can iterate through the details of each match

import re
myRegEx = re.compile('ab*')
myMatches = myRegEx.finditer("ffabbbcab")
for myMatch in myMatches:

Back Reference

This example finds matches for "56xx3" where each x is the same digit.

import re
myRegEx = re.compile('56(?P<a>\d)(?=Pa)3')
print(myRegEx.match("56003")) #matches 56003

The phrase "(?P<a>\d)" matches a digit (\d) and names it "a" (?P<a>).

The phrase "(?=Pa)" matches the same substring that "a" matched.


import subprocess
cmdLine = r"c:\path\myProgram.exe"
myProcess = subprocess.Popen(cmdLine)
muProcess.wait() #wait until process completes

Numpy Array

Numpy is a core python library. It is very commonly shortened to "np".

import numpy as np

x = np.array([0, 1, 2, 3]) #create a rank 1 array

print(type(x)) #outputs <class 'numpy.darray'>
print(x) #outputs [0, 1, 2, 3]
print(x[0]) #outputs 0
print(x.shape) #outputs (4,) because the length is 4

y = np.array([[1, 2, 3], [10, 20, 30]]) #create a rank 2 array

print(y[1, 2]) #outputs 30
print(y.shape) #outputs (2, 3) because the outer length is 2, the inner length is 3

Create an array filled with default values.

import numpy as np

x = np.zeros((1, 2)) #creates [[0, 0]]
y = np.ones((2, 2)) #creates [[1, 1], [1, 1]]
z = np.full((3, 1), 7) #creates [[7], [7], [7]]

Create an array filled with random values.

import numpy as np

x = np.random.random((2, 2)) #creates [[a, b], [c, d]] where those are random values


Try Catch

    x = input("a number:")
except IOError:
    print("IO Error")
except TypeError as e:
    print("TypeError: " + str(e)) #prints the error message
    print("some other error")
Note that "except" will catch even subclasses of the stated exception class. For example "except ArithmeticError" will catch subclass "FloatingPointError". View the Exception Hierarchy to make use of this.

Raise Error

raise ValueError("message")

Convention says do not raise generic Exception

Error Types

IOError, ValueError, ImportError, EOFError, KeyboardInterrupt

Custom Errors

"Errors" all inherit from the base "Exception" class.
The naming convention is that all errors/exception Types end with "Error".

Define a custom error type:

class MyError(Exception):
    def __init__(self, expression, message):
        self.expression = expression
        self.message = message

Seed random number generator with an integer or long.

import random

If you pass in None, the generator will seed with current time or something like it.

Get a random float from 0 inclusive to 1 exclusive.

import random

x = random.random()

Get a random integer from N to M, both inclusive.

import random

x = random.randint(0, 5)

Select a random value from a sequence.

import random

x = [1, 2, 3, 4, 5]
y = random.choice(x)

Randomly sort a sequence in place.
You can optionally provide your own random number generator function, provided it returns values [0, 1).

import random

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

random.shuffle(x, myRandomFunction)
Meta Or Analysis

Looks like "reflection" is called "introspection" in Python.

get the size of an in-memory object

import sys

size = sys.getsizeof(myVariable)


Using the datetime library.

import datetime

currentDateTime =
currentTime = currentDateTime.time()
Unit Tests

To create a unit test class, make a subclass of unittest.TestCase.
Each class method named with prefix "test" will be run as a test.

"setUp" is run before each test method.
"tearDown" is run after each test method, whether it succeeded or not.
"setUpClass" is run once before the class tests are run.
"tearDownClass" is run once after the class tests are run.

"unittest.main()" provides a command line interface to the tests.

import unittest

class MyTests(unittest.TestCase):
    def test_upper(self):
        self.assertEqual("foo".upper(), "FOO")
    def test_isupper(self):
    def test_split(self):
        #test that split fails without separater
        s = 'hey oooh'
        with self.assertRaises(TypeError):
    def setUp(self):
        print("set up")
    def tearDown(self):
        print("tear down")
    def setUpClass(cls):
        print("set up class")
    def tearDownClass(cls):
        print("tear down class")

if __name__ == "__main__":


assertEqual(a, b)
assertTrue(a == b)
assertFalse(a == b)

 #verify a specific error was raised

Command Line

You can run your tests in different ways.

The test case file contains the "unittest.main()" section, and will run itself.

(the ".py" is optional)

The test case file does not contain the "unittest.main()" section. This will run it.

python -m unittest

You can run multiple test files.

python -m unittest

You can run just one test class from a file the contains multiple classes.

python -m unittest MyTests.TestCustomer

You can run just one test method from a class.

python -m unittest MyTests.TestCustomer.test_payment

    -v verbose, more information shown
    -f fail fast, stop at first failed test
    -c catch, Control-C while tests run will (after current test completes) display results so far
    -b buffer, output is sent to a buffer to be saved
    --locals shows local variables in tracebacks
Test discovery: from the current directory, unittest will search here and below for tests to run

python -m unittest discover
If you don't add command options, then "discover" is optional.

Discover Options:
    -s specify a starting directory
    -p specify a pattern that the filename must match

Note that the file at path A/B/ will be imported as A.B.C    


Running unittest.main() will run all the tests in the file.
You can also build an explicit suite, and run just those tests.

import unittest

def suite():
    suite = unittest.TestSuite()
    return suite
class MyTests(unittest.TestCase):
    def test_a(self):
    def test_b(self):
    def test_c(self):
if __name__ == "__main__":
    runner = unittest.TextTestRunner()


You can mark test methods to be skipped.
You can use the same syntax to mark an entire class to be skipped.

import unittest

class MyTests(unittest.TestCase):
    def test_a(self):
    @unittest.skipIf(myVersion < 4, "message")
    def test_b(self):
    @unittest.skipUnless(operatingSystem == "Windows", "message")
    def test_c(self):

You can conditionally skip a test at any point.

import unittest

class MyTests(unittest.TestCase):
    def test_a(self):
        if not resourceIsSetup:

Expected Failure

You can mark a method as an expected failure. It will not be counted as a failure on the test result.

import unittest

class MyTests(unittest.TestCase):

    def test_a(self):

Sub Test

You can run multiple tests within a loop.

import unittest

class MyTests(unittest.TestCase):
    def test_even(self):
        for number in range(0, 10, 2):
            with self.subTest(x=number):
                self.assertEqual(x%2, 0)
Each iteration will be reported as a unique test, specifying the arguments used.
Even if one iteration fails, all the iterations in the method will be run.


run all tests recursive-within directory

pytest path/folder

run all tests in file

pytest path/folder/

run all tests that contain this string in the method name

pytest path/folder/

run all tests that contain this string in the method name, with these parameters filled in

pytest path/folder/[4]


Microsoft's recommended python library for accessing SQL Server.


pip install pyodbc
(Might need to run this at C:\Users\<YOUR NAME>\AppData\Local\Programs\Python\Python<VERSION>\Scripts)

Connect To Database

Using current user's Windows login:

import pyodbc 

connStr = (
    r"DRIVER={SQL Server Native Client 11.0};"
conn = pyodbc.connect(connStr, autocommit=True)

cursor = conn.cursor()
cursor.execute('SELECT ColumnA, ColumnB FROM dbo.myTable')

for row in cursor: #row is a Tuple

- I got a login error when actually I had a typo in the database name.

--SQL Server

ODBC Drivers

{SQL Server} - released with SQL Server 2000
{SQL Native Client} - released with SQL Server 2005 (also known as version 9.0)
{SQL Server Native Client 10.0} - released with SQL Server 2008
{SQL Server Native Client 11.0} - released with SQL Server 2012
{ODBC Driver 11 for SQL Server} - supports SQL Server 2005 through 2014
{ODBC Driver 13 for SQL Server} - supports SQL Server 2005 through 2016
{ODBC Driver 13.1 for SQL Server} - supports SQL Server 2008 through 2016
{ODBC Driver 17 for SQL Server} - supports SQL Server 2008 through 2017


Python implementation of the Qt library.
Not sure about the license, but it looks like the free PyQt isn't available for commercial applications.
UI Tkinter

The standard python UI library.
A wrapper around Tcl/Tk

You can check that tkinter is properly installed on your system by running "python -m tkinter" from the command line; this should open a window demonstrating a simple Tk interface.


Open an empty window:

import tkinter

class Window(tkinter.Frame):
    def __init__(self, master=None):
        tkinter.Frame.__init__(self, master)
        self.master = master
        self.master.title("Example") #title displayed on window
        self.pack(fill=tkinter.BOTH, expand=1) #layout of window
root = tkinter.Tk()
root.geometry("400x300") #size of window
app = Window(root)

Add a button, quit the app:

import tkinter

class Window(tkinter.Frame):
    def __init__(self, master=None):
        tkinter.Frame.__init__(self, master)
        self.master = master
        self.pack(fill=tkinter.BOTH, expand=1)

    def init_layout(self):
        quitButton = tkinter.Button(self, text="Quit", command=self.client_quit), y=20)
    def client_quit(self):

root = tkinter.Tk()
app = Window(root)

Add a main menu bar:

import tkinter

class Window(tkinter.Frame):
    def __init__(self, master=None):
        tkinter.Frame.__init__(self, master)
        self.master = master
        self.pack(fill=tkinter.BOTH, expand=1)

    def init_layout(self):
        menu = tkinter.Menu(self.master)
        file = tkinter.Menu(menu)
        file.add_command(label="Quit", command=self.client_quit)
        menu.add_cascade(label="File", menu=file)
    def client_quit(self):

root = tkinter.Tk()
app = Window(root)

Geometry Manager: Place

Place: arrange widgets by relative or absolute values

import tkinter

class Window(tkinter.Frame):
    def __init__(self, master=None):
        tkinter.Frame.__init__(self, master)
        self.master = master
        self.pack(fill=tkinter.BOTH, expand=1)
    def initLayout(self):
        self.inputControl = tkinter.Text(self, height=20, width=30), rely=0.5, anchor=tkinter.CENTER) #center widget in parent
root = tkinter.Tk()
app = Window(root)

import tkinter

class Window(tkinter.Frame):
    def __init__(self, master=None):
        tkinter.Frame.__init__(self, master)
        self.master = master
        self.pack(fill=tkinter.BOTH, expand=1)
    def initLayout(self):
        self.inputControl = tkinter.Text(self, height=20, width=30), y=10) #place absolute
        self.outputControl = tkinter.Text(self, height=20, width=30), y=200) #place absolute
root = tkinter.Tk()
app = Window(root)

Geometry Manager: Grid

Grid: arrange widgets in a 2d table

import tkinter

class Window(tkinter.Frame):
    def __init__(self, master=None):
        tkinter.Frame.__init__(self, master)
        self.master = master
        self.pack(fill=tkinter.BOTH, expand=1)
    def initLayout(self):
        self.inputControl = tkinter.Text(self, height=10, width=20)
        self.inputControl.grid(row=0, column=0)
        self.outputControl = tkinter.Text(self, height=20, width=30)
        self.outputControl.grid(row=1, column=1)
root = tkinter.Tk()
app = Window(root)

Empty rows and columns are ignored.

Widgets default to centered in their cells.
To change that, add "sticky" option with one of these cardinal directions: N,S,E,W,NE,NW,SE,SW,NS,EW,NSEW etc.

self.inputControl = tkinter.Text(self, height=10, width=20).grid(row=0, column=0, sticky=tkinter.W)

Additional options:
columnspan=number of column cells to span
rowspan=number of row cells to span

If you call grid() with no row or column specified, then the column defaults to 0 and the row to the first currently empty row.

    widget.grid_forget() #removes widget from grid layout
Stretch widgets to fill space:
- the important points here are
    (A) give a positive integer weight to any column or row you want to stretch
    (B) make the widget inside the cell sticky to the opposing walls

import parser
import tkinter
from tkinter import ttk

class Window(tkinter.Frame):
    def __init__(self, master=None):
        tkinter.Frame.__init__(self, master)
        self.master = master
        self.pack(fill=tkinter.BOTH, expand=1, padx=5, pady=5)
    def init_layout(self):
        self.frame_history = tkinter.Frame(self)
        self.frame_history.grid(row=0, column=0, stick=tkinter.EW)
        self.columnconfigure(0, weight=1)

        tkinter.Label(self.frame_history, text="Input").grid(row=0, column=0, sticky=tkinter.EW)
        ttk.Separator(self.frame_history, orient=tkinter.VERTICAL).grid(row=0, column=1, stick=tkinter.NS)
        tkinter.Label(self.frame_history, text="Output").grid(row=0, column=2, sticky=tkinter.EW)
        self.frame_history.columnconfigure(0, weight=1)
        self.frame_history.columnconfigure(2, weight=1)

        self.input_equation = tkinter.Text(self, height=20, width=30)
        self.input_equation.grid(row=1, column=0, sticky=tkinter.EW)
        self.append_output("Test Input", "Test Output")
    def append_output(self, equation, result):
        row = 1
        output_equation = tkinter.Text(self.frame_history, height=1, width=30)
        output_equation.insert(tkinter.END, equation)
        output_equation.grid(row=row, column=0, sticky=tkinter.EW)
        ttk.Separator(self.frame_history, orient=tkinter.VERTICAL).grid(row=row, column=1, stick=tkinter.NS)
        output_result = tkinter.Text(self.frame_history, height=1, width=30)
        output_result.insert(tkinter.END, result)
        output_result.grid(row=row, column=2, sticky=tkinter.EW)

root = tkinter.Tk()
app = Window(root)

Geometry Manager: Pack

(do not use Grid and Pack in the same window)

    widget.pack_forget() #removes widget from pack layout



widget.bind(event, handler)

You can call a global event handler (see examples below), or call self.handler to use a handler in your current class (in that case, the handler must have parameters for self and event).

Mouse left-click:

import tkinter

def callback(event):
    print("clicked at", event.x, event.y)

root = tkinter.Tk()
frame = tkinter.Frame(root, width=100, height=100)
frame.bind("<Button-1>", callback)

Keyboard press:

import tkinter

def key(event):
    print("pressed", repr(event.char))

root = tkinter.Tk()
frame = tkinter.Frame(root, width=100, height=100)
frame.bind("<Key>", key)

Event names are formatted as "<modifier-type-detail>"
    <Button-1> = left mouse click
    <Button-2> = middle mouse click
    <Button-3> = right mouse click
(Motion and Release events will be delivered to the widget where the mouse button was depressed, even if you move the mouse outside of it)
    <B1-Motion> = mouse is moved with left button held down
    <ButtonRelease-1> = left mouse button released
    <Double-Button-1> = left mouse button double clicked (<Button-1> will still be called)
    <Triple-Button-1> = left mouse button triple clicked
    <MouseWheel> = mouse wheel moved
    <Enter> = cursor entered widget
    <Leave> = cursor exited widget
    <Motion> = cursor is moving and staying in the widget
    <FocusOut> = widget lost keyboard focus
    <Key> = a keyboard key was pressed
    x = the "x" keyboard key was pressed (pattern works with other keys)
    <KeyRelease> = a keyboard key was released after being pressed
    <Shift-Up> = shift key is held down while the up arrow key is pressed (pattern works with other keys)
    <Alt-Up> = alt key is held down while the up arrow key is pressed (pattern works with other keys)
    <Control-Up> = control key is held down while the up arrow key is pressed (pattern works with other keys)
    <Return> = the "Enter" key was pressed
Special keyboard key names: Return, space, less (for less than), Cancel (for break), BackSpace, Tab, Shift_L (for either shift), Control_L (for either control), Alt_L (for either alt), Pause, Caps_Lock, Escape, Prior (for page up), Next (for page down), End, Home, Left, Right, Up, Down, Print, Insert, Delete, F1 (and so on), Num_Lock, Scroll_Lock
    <Configure> = widget changed sized
    <Activate> = widget state became active
    <Deactivate> = widget state became deactive
    <Destroy> = widget is being destroyed
    <Expose> = widget is now visually visible after being covered by something else
    <Map> = widget is being made visible in the application (like when you call .grid())
    <Unmap> = widget is being made not visible in the application
    <Visibility> = some part of the application window has become visible on the screen

Event object attributes:
    widget = the actual widget object that generated the event
    x and y = current mouse position in pixels
    x_root and y_root = current mouse position relative to the whole screen
    char = (for keyboard events) the key pressed, as a string
    keysym = (for keyboard events) the key symbol pressed
    keycode = (for keyboard events) the key code pressed
    num = (for mouse events) the mouse button pressed
    width and height = (for configure events) the new dimensions of the widget
    type = the event type
Binding levels:
    1. widget instance (use bind)
    2. widget's root window (use bind)
    3. widget's class (use bind_class)
    4. the application (use bind_all)
If your bindings overlap (such as <Key> and <Enter>), the most specific one is chosen.
If you have the same bindings at multiple levels (such as instance and all), both will be called. The lower level will be called first.

How to disable a standard event, such as typing a newline in a text field? That default event is at the widget class level.

def ignore(event):
    return "break" #use this specific return value to cancel the propagation of the event
myText.bind("<Return>", ignore)


myText.bind("<Return>", lambda event: "break")

 #or, to actually block all <Return> events on all Text widgets
 #this is not suggested
top.bind_class("Text", "<Return>", lambda event: None)


Methods and options general to all ui widgets.

Set focus on widget/control:

x = tkinter.Text(self)

Get real time widget dimensions:

print(widget.winfo_width(), widget.winfo_height())

For containers: a list of child widgets is in attribute "childen.values()"


The Text control/widget: Text(parent, options...)

Remember that width and height are in character units. There is NO easy way to specify pixel units - everyone suggests setting the parent container size and packing the text into it.

bg=background color
bd=border width (default 2px)
cursor=hover cursor
exportselection=1 or 0 (default 1 allows user to copy text from control)
font=font object
fg=foreground (text) color (can be changed for sections of text)
height=number of rows of text
highlightbackground=color of highlights when widget is out of focus
highlightthickness=thickness of the highlight, default 1, set to 0 to hide highlight
insertbackground=color of "insert" cursor, default black
insertborderwidth=size of "insert" cursor border, default 0
insertofftime=milliseconds "insert" cursor blinks off per cycle, default 300
insertontime=milliseconds "insert" cursor blinks on per cycle, default 600
insertwidth=pixel width of "insert" cursor, deafult 2px (height is set to tallest char in line)
padx=internal padding on left and right in pixels, default 1px
pady=internal padding on top and bottom in pixels, default 1px
relief=3D appearance of widget, default SUNKEN, can be SUNKEN, RAISED, GROOVE, RIDGE, FLAT
selectbackground=background color of selected text
selectborderwidth=pixel width of border around selected text
spacing1=extra vertical space above lines, default 0, does not affect a wrapped line
spacing2=extra vertical space above wrapped lines, default 0
spacing3=extra vertical space below lines, default 0, does not affect a wrapped line
spacing4=extra vertical space below wrapped lines, default 0
state=NORMAL(responds to keyboard and mouse events) DISABLED(does not, also cannot edit programmaticly)
tabs=?something about tab characters?
width=width measured in characters
wrap=WORD(wrap line on word break) CHAR(wrap line on any character)
xscrollcommand=?used to add a horizonal scroll?
yscrollcommand=?used to add a vertical scroll?

delete(startIndex [,endIndex]) #deletes a range of text
get(startIndex [,endIndex]) #gets a range of text
index(index) ???
insert(index, string) #insert text at index
see(index) #returns true if the text at this index is visible
mark_set("insert", index) #move cursor to an index in the text

Indexes: there are many ways to index a Text widget
    Line index starts at 1
    Character index within a line starts at 0

print(text.get("1.0")) #prints the character from line 1 at array-index 0

index = "%d.%d" % (line, column)

print(text.get(tkinter.CURRENT)) #character just before the cursor

print(text.get("1.0", tkinter.END)) #first line of text

todo: see more indexing at


import tkinter

class Window(tkinter.Frame):
    def __init__(self, master=None):
        tkinter.Frame.__init__(self, master)
        self.master = master
        self.pack(fill=tkinter.BOTH, expand=1)
    def initLayout(self):
        tkinter.Label(self, text="Input").grid(row=0, column=0)
        tkinter.Label(self, text="Output").grid(row=0, column=1)
root = tkinter.Tk()
app = Window(root)

    anchor = the cardinal direction to justify the text to: N,S,W,E
    justify = ? not sure how this works with anchor: LEFT, RIGHT


A vertical or horizontal 2px wide separator.
Make sure to set the "sticky" option, otherwise the separator will be 1px long.

import tkinter
from tkinter import ttk

class Window(tkinter.Frame):
    def __init__(self, master=None):
        tkinter.Frame.__init__(self, master)
        self.master = master
        self.pack(fill=tkinter.BOTH, expand=1, padx=5, pady=5)
    def init_layout(self):
        tkinter.Label(self, text="Input").grid(row=0, column=0, sticky=tkinter.W)
        ttk.Separator(self, orient=tkinter.VERTICAL).grid(row=0, column=1, stick=tkinter.NS)
        tkinter.Label(self, text="Output").grid(row=0, column=2, sticky=tkinter.W)

root = tkinter.Tk()
app = Window(root)


Scrollbars can only be associated with a few widget types: List, Textbox, Canvas, and Entry

To use scrollbars with other widgets, or layouts of widgets, the usually solution seems to be to create a scrolling canvas, embed one frame in the canvas, and embed your other widgets in the frame. Make sure the dimensions of the frame are fed into the canvas "scrollregion" option.

from tkinter import *






canvas.config(xscrollcommand=hbar.set, yscrollcommand=vbar.set)


General use scrolling canvas - put anything inside the "inner_frame":

import tkinter
from tkinter import ttk

class ScrollFrame(tkinter.Frame):

    def __init__(self, master):
        tkinter.Frame.__init__(self, master)
        self.master = master

        self.canvas = tkinter.Canvas(self)
        self.canvas.grid(row=0, column=0, sticky=tkinter.NS)
        #self.canvas.grid_propagate(False) #don't let the inner canvas resize ?
        self.scrollbar = tkinter.Scrollbar(self, orient="vertical", command=self.canvas.yview)
        self.scrollbar.grid(row=0, column=1, sticky=tkinter.NS)

        self.inner_frame = tkinter.Frame(self.canvas)
        self.inner_frame.grid(row=0, column=0, sticky=tkinter.NS)

        self.canvas.columnconfigure(0, weight=1)
        self.canvas.rowconfigure(0, weight=1)

        self.canvas.create_window(0, 0, window=self.inner_frame, anchor='nw')
    def update_scroll_region(self):
        self.master.update() #make sure the layout is completely calculated
        self.canvas.configure(scrollregion=self.canvas.bbox("all")) #get a bounding box around all child widgets
full example:

import parser
import tkinter
from tkinter import ttk

class ScrollFrame(tkinter.Frame):

    def __init__(self, master):
        tkinter.Frame.__init__(self, master)
        self.master = master

        self.canvas = tkinter.Canvas(self)
        self.canvas.grid(row=0, column=0, sticky=tkinter.NS)
        #self.canvas.grid_propagate(False) #don't let the inner canvas resize ?
        self.scrollbar = tkinter.Scrollbar(self, orient="vertical", command=self.canvas.yview)
        self.scrollbar.grid(row=0, column=1, sticky=tkinter.NS)

        self.inner_frame = tkinter.Frame(self.canvas)
        self.inner_frame.grid(row=0, column=0, sticky=tkinter.NS)

        self.canvas.columnconfigure(0, weight=1)
        self.canvas.rowconfigure(0, weight=1)

        self.canvas.create_window(0, 0, window=self.inner_frame, anchor='nw')
    def update_scroll_region(self):
        self.master.update() #make sure the layout is completely calculated
        self.canvas.configure(scrollregion=self.canvas.bbox("all")) #get a bounding box around all child widgets

class Window(tkinter.Frame):
    def __init__(self, master=None):
        tkinter.Frame.__init__(self, master)
        self.master = master
        self.master.title("Python Calculator")
        self.pack(fill=tkinter.BOTH, expand=1, padx=5, pady=5)
    def init_data(self):
        self.list_equations = []
        self.list_results = []
        self.index_history = None
    def init_layout(self):
        self.scroll_frame = ScrollFrame(self)
        self.scroll_frame.grid(row=0, column=0, sticky=tkinter.NS)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        self.frame_history = tkinter.Frame(self.scroll_frame.inner_frame)
        self.frame_history.grid(row=0, column=0, sticky=tkinter.EW)
        tkinter.Label(self.frame_history, text="Input", anchor=tkinter.W).grid(row=0, column=0, sticky=tkinter.EW)
        ttk.Separator(self.frame_history, orient=tkinter.VERTICAL).grid(row=0, column=1, stick=tkinter.NS)
        tkinter.Label(self.frame_history, text="Output", anchor=tkinter.W).grid(row=0, column=2, sticky=tkinter.EW)
        self.frame_history.columnconfigure(0, weight=1)
        self.frame_history.columnconfigure(2, weight=1)
        self.input_equation = tkinter.Text(self.scroll_frame.inner_frame, height=3, width=30, relief=tkinter.FLAT)
        self.input_equation.grid(row=1, column=0, sticky=tkinter.EW)
        for i in range(0, 25):
            self.append_output("aaaaaaa", "bbbbbbb")
    def append_output(self, equation, result):
        row = len(self.list_equations) + 1
        output_equation = tkinter.Text(self.frame_history, height=1, width=30, relief=tkinter.FLAT)
        output_equation.insert(tkinter.END, equation)
        output_equation.grid(row=row, column=0, sticky=tkinter.EW)
        ttk.Separator(self.frame_history, orient=tkinter.VERTICAL).grid(row=row, column=1, stick=tkinter.NS)
        output_result = tkinter.Text(self.frame_history, height=1, width=30, relief=tkinter.FLAT)
        output_result.insert(tkinter.END, result)
        output_result.grid(row=row, column=2, sticky=tkinter.EW)

root = tkinter.Tk()
app = Window(root)

Scroll to top of area:


Scroll to bottom of area:



Python 3 uses the tkinter.font library.
You must create your root window (tkinter.Tk()) before you create your font.

import tkinter
from tkinter import font

root = tkinter.Tk()
default_font = font.Font(family="Helvetica", size=10, weight="normal")

UI WxPython

Python implementation of the wxWidgets library. See the Phoenix Project if using Python 3.
Images - Pillow

Pillow library (fork of PIL Python Image Library). Pillow and PIL cannot be installed at the same time.

[Installation Instructions]

Getting Started

From existing image:

from PIL import Image
img ='myImage.png')

Create new image:

from PIL import Image
img = = 'RGB', size = (60, 30), color = 'red')'../output/test.png')
"size" is tuple (width, height)
"color" sets the initial background color


1 - (1-bit pixels, black and white, stored with one pixel per byte)
L - (8-bit pixels, black and white)
P - (8-bit pixels, mapped to any other mode using a color palette)
RGB - (3x8-bit pixels, true color)
RGBA - (4x8-bit pixels, true color with transparency mask)
CMYK - (4x8-bit pixels, color separation)
YCbCr - (3x8-bit pixels, color video format) Note that this refers to the JPEG, and not the ITU-R BT.2020, standard
LAB - (3x8-bit pixels, the L*a*b color space)
HSV - (3x8-bit pixels, Hue, Saturation, Value color space)
I - (32-bit signed integer pixels)
F - (32-bit floating point pixels)


from PIL import Image, ImageDraw

image ='RGB', (800,600), 'white')
draw = ImageDraw.Draw(image)
draw.line([(0,0),(100,400)], fill='gray', width=1)'../output/test.png')
The list of point-tuples can be as long as you need.


There's not much support for anti-aliasing.

One suggestion: The only way to do it natively is with supersampling. Render your image at a multiple of the size you require, then resize it with filter=Image.ANTIALIAS.

Images - ImageIO

[imageio Home Page]

Save Animated GIF

Must start with all "frames" saved to file system.

    using imageio

    #imageFilenames is a list of filenames for the "frames"
    images = []
    for imageFilename in imageFilenames:
    imageio.mimsave(saveToFilename, images, fps=framesPerSecond)


PyLint will only run on *.py files in a folder that contains an file.

Disable Warnings

The {warning-type} will be printed at the end of each PyLint warning in the report, such as (invalid-name) or (consider-using-set-comprehension).

to disable a warning type for a whole file, place these at the top of the file

Pylint: Disable={warning-type}

to disable a warning on one statement, place this at the end of the lin

x = my_function()  # pylint: disable={warning-type}