Python for Programmers

Python is a "scripting" language. This means it doesn't have funny types like unsigned short int who's only purpise is to save space. Commands focus on being easy to use and aren't always the most efficient. As a reaction to overcomplicated Perl, Python's feature set is minimal. There isn't even a do-while loop. It's also famous for having mandatory indentation. But the most unusual feature is that there's no type safety -- any variable can be any type at any time.

Variables are generic pointers to typed objects

Python data has types, but the variables don't. Because of that, they aren't even declared. Assigning to a variable creates it out of thin air. Examples:

n=4
type(n)  # <class 'int'>

n="cow" # assigning n to a different type is completely legal
type(n) # <class 'str'>

The language isn't completely insane. Trying to read from a non-existent variable gives an error. In n=m Python won't create m on the fly.

The no-type rule also applies inside of lists. There no such thing as a list of ints, only a generic list-of-whatever:

n=[4, 'frog', 4.5]  # different types in a list is legal
type(n)  # <class 'list'>, notice the lack of element type

Python often uses lists as a cheap way to avoid making a class. Here we abuse lists to give each cat a name and age, and even go nuts by including the name of the list as the first item:

Cats=[ "vet appointments", ["fritz",7], ["jub-jub",3] ]

Function parameters use the same rule -- no types. This means all Python functions are automatically virtual, or "duck-type". This legal function attempts to run whichever + will work on the inputs:

def sum(n1,n2):  # <- these parameters don't have types
  return n1+n2

sum("cat","fish")  # catfish

This function expects a list of numbers. It can take ints, floats or a mix of either. That's nice. But it can also take non-lists, which will crash it:

def posCount(L):  # L should be a list of numbers
  count=0
  for n in L:
    if n>0: count+=1

  return count

posCount( [-4, 3, 5, -2] )  # all ints
posCount( [4.7, 2.3, 99] )  # mixed float ints
posCount(-14)  # legal, but will crash

Basics

Basic types

Numeric types are int and float. That's it. Division is a bit odd: / is real division, // is int division, and modulo(%) works with float:

n=3;  f=8.2  # "declaring" an int and a float

9/2    # 4.5  single slash is real division
9//2   # 4    double slash is int division...
9.1//2   # 4.0 ... even with floats

8.5 % 2.1  # 0.1  (8.4, with 0.1 remainder) modulo with floats

++ is gone (and --). But we get the old ** for exponentiation: 2**4 is 16. Mixed int/float often takes care of itself, for example: n=3; n+=0.6 is fine: n was an int, but changes into a float.

Because of integer division's //, we get a really cool-looking //= operation:

n=28;  n/=10  # 2.8, oops! used real division
n=28;  n//=10 # 2

# Hmmm...int division on a float rounds but keeps as a float:
47.4 //= 10  # 4.0

Bools work the usual way, except they use capital True and False as in: happy=False.

Strings are str. They also work the usual way, but there's no char type! Strings are made of length 1 strings. "cat"[0] is the string "c". Like javascript, strings can use single or double quotes (which must match). Strangely, length is a normal function -- len(w) (the length of everything in Python works that way):

w1="camel"; w2='clutch'  # single or double quotes
type(w1)  # <class 'str'>

w3=w1+w2;  w3+="er"  # strings and + are normal

ch=w1[0]   # "c",  ch is just a small str
len(w1)    # 5  length is not a member function

All indexing in Python has an option to count from the right, w1[-1] is the last element, w1[-2] is the second-to-last, and so on. "abcde"[-2] is the string "d".

String indexes can't be assigned to. w[i]='x' is an error. You'll need to assign w to a new string with that change.

Lists

The only list type is list. There's no fixed-length array or linked-list (the list type is array-backed). Lists are created using []'s and commas. As usual, you don't declare them, you just make them:

N=[2,5,8]  # all there is to making a list
A=["car","truck","boat",plane"]
B=[1.6, 2, "frog", 6, ["x","y"], 7]  # mixed types are allowed

Indexing works as normal, including negative going from the right (L[-1] is the last item). They're range-checked -- L[999] throws an error. They allow insert and remove from anywhere, but preferably at the end:

Nums = []  # empty list

Nums.append(7)  # add a single item to the end
Nums.extend( [9,9,9] )  # add contents to end: [7,9,9,9]
# compare: Nums.append( [9,9,9] ) gives [7, [9,9,9] ]
Nums.insert(2, 942)  # add 942 before old slot #2

len(N)  # 5  [7,9,942,9,9]

n=Nums.pop()  # remove from end
Nums.pop(3)  # remove that index
Nums.remove(942)  # search for and remove that item

N+M is a shortcut for N.extend(M): [1,2,3]+[4,5,6] is a 1-6 list

All list-like objects use in as the contains check:

L=[2,6,9,12]
if 9 in L:  # True
5 in L  # False

hasX = "x" in "eckzactly"  # False

Tuples

Tuples are read-only lists, using parens instead of square brackets. Python uses these whenever it can. They work the same as lists:

c1 = ("fluffy", 8, 1.4)  # a tuple
len(c1)  # 3
c1[0]; c1[-1]  # "fluffy"  1.4 (1st and last)

type(c1)  # <class 'tuple'>,  again, notice absence of element type

c1[2]=5.6  # Error. Tuples are read-only

The ()'s around a tuple are optional. Whenever it looks as if multiple values are being sent, it's really sending 1 tuple:

a = 1, 9, 12  # same as a=(1,9,12)
a[2]  # 12 -- a really is a 3-part tuple

# at the end of some function:
  return w, "bird", True  # really: (w,"bird",True)

We'll see more tricks with this later.

Dictionary

Python dictionaries are the usual. They're made with curly-braces and key:value pairs. Assign and look-up use []'s. keys() and values() retrieve them:

D = { "cat":8, "horse":275, 'bird':2 }  # a dictionary
D['horse']  # 275
D["goat"]=140  # add new item

list(D.keys())  # ['cat', 'horse', 'bird']

Reading a non-existent dictionary item is an error. Luckily in works with dictionaries:

a='frog'
wt=D[a]  # error, no frog

# this is safe:
if a in D: wt=D[a]
else: wt=-99

Reference/Value Types

Python uses the standard Reference Type scheme (like Java). int, float, bool, str act as value-types, everything else is a pointer. Contrary to the usual way, == is a value-compare, while is checks pointer equality:

N1 = [1,2,3];  N2 = [1,2,3]  # different lists, with same contents

N1==N2  # True, same contents
N1 is N2  # False, not same objects
N1=N2  # both are now the same object
N1==N2; N1 is N2  # True True

Python's null is: None: c1=None. The preferred null-test is if c1 is None:. None is actually pre-made as the only value of the type NoneType. All variables which have been set to None point to it.

Python doesn't like to say Value and Reference type. It prefers the more confusing immutable and mutable, then says crazy things like "ints are immutable". That's just a fancy way to say that Python uses the same trick as Java and everyone else.

Technically, n=5; n+=2 doesn't change the 5 to a 7. It actually creates a 7 somewhere else and moves n to point to it. Likewise w="cow"; w+="bell" leaves the "cow" alone, creating a fresh "cowbell". So technically ints and strings can never change. But the entire point of the trick is to make pointers that act like value types. And it works. Everywhere you see immutable in a Python book, you can cross it out and put Value Type.

It's a little fun to to see how smart Python is about sharing things:

w1="cow"; w2="cow"
w1 is w2  # True -- w2 noticed we already had a cow and shared

# if we make "cow" in 2 parts, Python doesn't notice the old cow:
w3="c"; w3+="ow"
w3==w1  # True -- both are "cow"
w3 is w1  # False -- w3 didn't realize it could share

Numbers are even stranger. Like Java, Python pre-makes small ones. Here we make 3 in sneaky ways. All share the same 3:

n1=3;  n2=7-4;  n3=30; n3//=10  # all are 3
n1 is n2; n2 is n3  # True True

But if we make a pair of large numbers, not even being tricky at all, Python doesn't bother checking if they can share:

n1=758;  n2=758
n1==n2;  n1 is n2  # True False -- 2 copies of 758

# proving they can share:
n1=682;  n2=n1
n1 is n2  # True

This is all a long way of saying you can use is with "value types", but don't since it's totally unreliable.

if's

If statements don't use parens and end in a full colon. They have the usual compare operators (<=, ==, !=), but boolean logic uses written out "and", "or", and "not":

if w!="cow":
  print("no cow")

if not(n>=0 and n<=10):  # "not" "and"
  print("outside of 0-10")
  
if n==3 or n==7 or n==15: print('lucky')

Strangely, Python uses the old elif instead of else-if:

if n<10: print('low')
elif n<25: print('med')
else: print('hi')

Comparisons may be chained: 0<n<100 is actually legal and does what you think! They evaluate in pairs from left to right, and'ed together. It works for any compares, in a chain of any length:

if 10<=n<=20: ... # same as n>=10 and n<=20
if 0 <= i < len(A):  # a basic 'legal index' check

# test whether A is ascending:
if A[0]<A[1]<A[2]<A[3]<A[4]: ...

if a==b==c==0: ... # all are 0's

# mixed operators are allowed (but what a mess):
if x<y==z>=50:  ... # True for: 4,56,56

The technical term is interval comparison. It's lazy: 1 == 2 < 1/0 safely evaluates to False.

They can be confusing. if x!=y!=z!=0: looks like it checks for all non-zero but is actually junk. It only checks adjacent pairs so is true for things like [0,2,1] and [1,0,3] (x and y aren't the same, neither are y and z, and z isn't 0).

Python has the value-returning ?:, but it's more awkward than in other languages. The compare goes between the values:

w=  "positive" if n>=0 else "negative"

loops

Python has only 2 loops: while and for, which is really a foreach. If you're used to normal for-loops, seeing Python's for is confusing.

Whiles are nothing special. Like IF's, they don't use parens and end in a full colon:

x=1
while x<100:
  print(x)
  x*=2

The for loop (which is a foreach) can loop through any sort of sequence, even strings:

for x in [3,6,9,12]:  # looping through a list
  print(x)

for ch in "camel":  # every letter
  if ch=="a": aCount+=1

Typical counting loops can be faked using the range function. Range stops before the last number and defaults to starting at 0:

for n in range(10): print(n)  # 0 through 9
    range(90,100)  # 90 through 99
    range(20,30,2) # 20,22,24,26,28 (not 30)
    range(10,0,-2)  # 10,8,6,4,2 (not 0)

Range doesn't create a list -- it creates a range iterator -- but can easily cast into a list. list(range(10)) makes a real 0-9 list

functions

Functions start with def. The header ends with a full colon. Parameter names are listed, but with no types. Nothing lists the return value or whether there is one:

def maxOf3(a,b,c):
  if a>b:
    if a>c: return a
    return c
  if b>c: return b
  return c
  
max(5,9,4)
max("cat", "panther", 'cougar')  # > on str compares the length

Functions with no return send back None (Python's null). In other words, there are no void functions -- they always return something. You might recall that None is a new type. That's no problem -- Python functions can return any type, any time.

Default values use the usual = syntax: def f(n=0). In a call, name=value is used for Named Parameters (the same as how some other languages use f(n:3)):

def g(a=99, b=99):  # setting default parameters

g()  # works as normal -- using both 99's

g(b=16, a=3)  # named parms: same as g(3,16)
g(b=12)  # both! same as g(99,12)

There's no call-by-reference. But we rarely need it -- we mostly pass reference types anyway.

white-space

In Python, end-of-line ends statements. As we've seen, a full-colon is used for block headings: ifs, loops, functions, classes. Python has semi-colons as seperators if you want to put several statements on one line. Famously, blocks must be indented. Everything in a block must have the same indent (obviously), but the amount can vary for different blocks. Blank lines don't do anything:

if b>c: 
  b=0  # we choose a small indentation
  c=99; print("semicolon seperates same-line statements")
  
  if(d>10):
         d=0  # indented more here, since we felt like it
         c=999	 
  e=0 # back to 1st block (same indent is required)

Python has a problem with empty blocks -- how can we stop indenting if we never started? As a fix we have a no-op named pass:

def doNothing():
  pass  # <-- the official Python "do nothing"
  
if a>99: pass
else: print("a is small enough")

Since line-breaks end statements, we can't split a statement over 2 lines. Python revives the old backslash line continue:

n = cats*2 + dogs*12 + \  # to be continued
    camels  # the indent here is unimportant

casting

Casting is the usual: str(43) is "43", int(4.9) is 4, int("12") is 12. To safely use these we have isdigit for ints, and try/catch for floats:

"436".isdigit()  # true -- contains only digits

if w.isdigit(): n=int(w)
else: n=-999

try: n=float(w)
except ValueError: n=-999
# in Python, float("abc") throws ValueError

Strings combine with +, but there's no implicit cast from numbers. "slot"+3 is an error. Either cast yourself, or use format:

w= "x=" + str(x) + " y=" + str(y)  # no implicit cast to str

# format fills {}'s with the input, casting to string:
w= "x={} y={}".format(x,y)

# {0} uses the first input, and so on:
phrase="There's a {0} in here. A {0}. Someone {1} that {0}!"
phrase.format("bear", "wrestle") # There's a bear in here ...

Python can pretend length-1 strings are characters and cast then to and from ASCII with ord and chr:

ord("a")  # 97
chr(98)  # "b"

w="c"
chr(ord(w)+7)  # "j"

# not quite right:
str(ord(w)+7)  # '106' opps! turned 106 into "106"

Most list-like types cast back and forth, string to list, list to tuple and so on:

A = list("frog")  # ["f", "r", "o", "g"]
B = tuple( [5,6,7] );  # (5,6,7) -- which is a tuple

# range creates an iterator, which can be cast into a list:
C = list( range(5) )  # [0,1,2,3,4]

# list(dict) returns the keys, just because:
list( {"hammer":8, "wrench":12}  # ['hammer', 'wrench']

Variables can also point to types. No special syntax -- just assign them. Check them using is:

t1=int

t2 = type(n)
if t1 is t2: ...
elif t1 is float: ...

Fun fact: the type of variables holding a type is type. For example: n=type(t1); type(n) gives < class 'type'>.

More tuples and Multiple assign

Python allows multi-assign through tuples. Since most people use the no-parens shortcut, it looks like every other multi-assign:

a, b = 5,12  # a=5; b=12
(a,b) = (5,12)  # what the computer really sees

N = [5,8,"cow"];
x,y,z = N  # x is 5, y is 8, z is "cow"

a,b,c = "CAT"  # a="C", b="A", c="T"

x,y = y,x  # a swap!!

Notice how multi-assign works on anything with a len. For strings it treats each letter as an item.

Functions which appear to return multiple values are actually returning tuples. Users are allowed to catch them as single tuples, or with multi-assign:

def f(n):
  ...
  return val, status
  
v1, s1 = f(8)  # catches the returned val and status
# or catch a tuple in a tuple:
res = f(8)  # res is a tuple. res[1] is the status

The sizes must match exactly, but we have two tricks to get around that. By convention, underscore is used as a dummy variable. That's not much of a trick -- you still need to know the exact size -- but it can sometimes help:

x, y, _, _ = N  # first 2 items, N must be length 4
_, age, wt = C  # last 2 items in C, which must be len 3

The real trick is adding a star before a variable to "glob" it -- it matches 0 or more items, becoming a list. This lets us multi-assign from variable length lists:

x, y, *_ = N  #  get first 2 items, N can be longer than 2  

*_, x, y = N  # last two items
x, *_, y = N  # first and last

# get 2nd-to-last and 3rd-to-last items:
*_, n1, n2, _ = N

As a note, x,y = N[0], N[-1] is probably an easier way to get the first and last.

We often use * with underscore to ignore the globbed part, but the globbed part is returned as a list which we can capture:

a, *b, c = "FROGMAN"  # a="F", c="N",   b=["R", "O", "G", "M", "A"]
*x, y = [1,2,3]  # x=[1,2];  y=3
*x, y = "z"  # x=[], y="z"

You're allowed to dig into the structure using nested ()'s. ((x,y), *_) would get 4 and 5 from [ [4,5], 8, 7, 3]. Here's a tricky one. It gets the first 2 letters of the second word (read it in 2 steps: get 2nd word, then inside get 1st 2 letters):

_,(ch1, ch2,  *_), *_ = ["ant", "monkey", "duck", "pig"]
# ch1="m", ch2="o"

for loops can deep multi-assign when going through a list of lists. Assume Cats are in a list like (("Yaz",5, 7.2), ("Mittens",2,6.5), ...]. This loop pulls out each cat into 3 vars:

for name, age, wt in Cats:
  # name="Yaz", age=5, wt=7.2, and so on

# 1st letter of each word:
for ch,*_ in ["ant", "bear", "cow"]:  # 'a', 'b', 'c'

As usual, if each item in the list doesn't have exactly 3 parts, the Cat loop will crash. The second example crashes on empty strings.

Loops can pattern-match as deep as you need. For example, for (ch,*_),*_ in Cats picks out the first letter of each cat's name (but doing it by hand might produce more readable code).

namespaces, importing modules

Python doesn't allow you to use anything without importing it first. Then you can use it with its full path:

import random  # a python built-in for random numbers

x=random.randInt(10,20) # random-dot is required

A standard "include" directive looks like from X include Y. We can then use imported items "naked":

from random import randInt, uniform

x=randInt(10,20)
y=uniform(1.5, 3.2)

Python doesn't have syntax to make namespaces because files and the file structure do it. A file is a namespace using the file name. Python calls it a module. Here's a complete cats which we can import:

== cats.py: ===
totalCatCount=0

def randomCatName(): return "fluffy"

== a program using it: ==
import cats

def newCat():
  cats.totalCatCount+=1  # use imported variable
  newName=cats.randomCatName()  # use imported function

Nested modules are made by creating nested directories (the same way as Java). If you want Cats to be in the "namepsace" Pets, put it in a directory named Pets. It's new name will be pets.cats. Pets with everything under it is called a package.

Here's where it gets ugly. Python only recognizes a package if you put an empty file named __init__.py in each directory (double-underscores around init):

Pets
  __init__.py
  cats.py
  dogs.py
  Other       <-- if we had another directory...
    __init__.py       ...it needs it's own init
    hamsters.py

import pets.cats
pets.cats.totalCatCount=4

The __init__ rule allows you to have subdirectories which aren't in the package (don't give them an __init__ file).

Finally, Python only looks for imports in it's current path variable (or the current directory). You'll need to put the root of your packages into it. It's in sys.path which is a list of strings. The code below adds a new location:

import sys  # oh, no -- requires another import

sys.path +=["/Users/bob/myPythonFiles"]

The other funny thing about how Python finds things is that there's no block scope. Loop variables or anything declared inside of an IF continue to exist. But this is rarely a problem: you don't have to worry about declaring them twice, and Python gladly changes the type:

for i in [1,7,8,9]: ...
# i is 9 after the loop

for i in "tree frog": ...  # i changes into a char
# i is "g"

Here's a crazy example of that (which isn't useful for anything). temp only exists if we take the IF:

if x<10:
  temp=8

n=temp # either 8, or gives a "no such variable" error

More on loops

Loops can have an else. It's very silly. It only fires when the loop exits normally -- as opposed to through break. It's for search loops where break means they found what they wanted. The else is "else if not found". Ex:

# does A have a positive number?:
for x in range(len(A)):  # each index in A
  if A[x]>0:
    print('found a positive number at '+str(x))
    break
else: # this only runs if the loop goes off the end of A
  print("no positive values in list")

I warned you it was silly. Notice how indentation clearly establishes what the else matches.

We can grab the value and the index using for i, val in enumerate(A,0). Enumerate takes a list and a starting index and creates a list of tuples:

W=["red", "green", "blue"]  # sample list
list( enumerate(W, 0) )
[(0,"red"), (1,"green"), (2,"blue")]

Notice how the index confusingly goes second in the call, but is first in the result. Also, enumerate technically gives an iterator, not a list (which is why we needed list( ) above to display the result).

The enumerate trick seems silly, but it's useful since a Python index loop is so ugly (for i in range(len(A)):. Yuck).

Function special pack/unpack

When calling a function, Python can convert lists and dictionaries into parameters, or the reverse -- a total of 4 possible conversions. To convert any number of inputs into a list, add a star before the last one (what many languages call variadic parameters):

def ff(n, *L):  # takes 1 or more parameters

ff(1,"cat",2,"dog")
# n is  1
# L is  ["cat", 2, "dog"]

To do the reverse (convert a list into several parameters), add a star before the list in the call (the function needs no extra work to be callable this way). For example:

def f(a,b,c):  # a normal 3-input function

A=[8,5,12]
f(*A)  # unpacks to become f(8,5,12)

f(*"cow")  # also legal, since it becomes: f("c", "o", "w")

B=[4,8];  f(5, *B )  # also legal: f(5, 4, 8)

It doesn't seem like much, but it saves typing out f(A[0], A[1], A[2]).

The other two versions are about using parameter names. A dictionary can be unpacked into named parameters by putting a double-star in front. As before, it works with any normal function:

def makeCat(name, age, cuteness):  # a normal function

# someone gives us a dictionary with the exact right names:
Cat1Info={"cuteness":7, "age":4, "name":"salem" }

makeCat(**Cat1Info)
# same as: makeCat(cuteness=7, age=4, name="salem")

This isn't useful for ordinary programs, but often some other program gives you inputs like Cat1Info, in dictionary format. This trick provides an easy way to use them.

The reverse of that trick is for a function to say it will take parameters with any names. Add a double-star in front. The parameters will be turned into a dictionary:

def fTakesAnyNames(**A):
  print(A)

fTakesAnyNames(cow=5, duck="quack")
# {"cow":5, "duck":"quack"}

fTakesAnyNames(red=1, purple=4, mediumGreyBlue=-3)
# {"red":1, "purple":4, "mediumGreyBlue":-3}

The trick is called keyword arguments, or kwargs. It's total fluff. We're sending the function a list of strings and values, which we could do plenty of ways, but it's so cool doing it like this.

For fun, here we see the dictionary tricks working together. Depending on the inputs, makeCat assigns what it can, putting the rest into its kwargs dictionary:

# the final **C says this also takes any named inputs:
def makeCat(name, age, cuteness, claws=5, **C): ....

Cat1Info={"size":4, "cuteness":7, "age":2, "hats":True }

makeCat("Tina", **Cat1Info, color="grey")

# name="Tina", age=2, cuteness=7, claws=5
# C={ "size":4, "hats":True, "color":"grey" }

More on Functions

As you'd expect, Python functions have access to file-level variables. For example:

dogs=0

def checkDogs(n):
  if n<dogs: print("arf")

But if a function wants to change a file-level variable, there's a crazy problem -- Python assumes you want to create a local. It's a consequence of the no-declare rule:

dogs=0
cats=0

def addDog():
  cats=8  # create _local_ cats in the function
  dogs+=1  # ditto -- makes _local_ dogs

That's a weird problem. The fix is a new, janky global command, instructing Python to look in the global scope:

def addDogs(n):
  global dogs  # don't declare any local 'dog' variables
  dogs+=n  # adds to global dogs

Functions can have an overall description, plus one for each input and for the output. They use crazy shortcuts. The overall description is an optional bare string as the first line in the body:

def max(a,b):
  "finds largest of the inputs using > to compare"
  if a>b: return a
  return b

The rule for paramters isn't too bad: they go after each, following a colon:

def chofinate(L1:"any list", L2:"a list of integers"):

The return value comment can go at the end of the first line, after a ->. It's sneaky because the -> really, really looks like it means something, but it's just part of the comment:

def max(a,b) -> "either a or b"

Every optional comment together looks like:

def swap(L:"a list", i1:"an index", i2:"an index") -> "no output"
  "switches positions i1 and i2 in L"
  L[i1], L[i2] = L[i2], L[i1]  # a cool python swap

In theory, an IDE can pop those up for us. For more detail, these are shortcuts for assigning to f.__doc__ and f.__annotations__ (double underscores). The first is a string, the other is a dictionary. You can set them directly if you want:

swap.__doc__="switches the items in a list"
swap.__annotation__["L"]="a list"
swap.__annotation__["i1"]="an index"

The annotations can be types. This can make it look very official, but here int and str are purely decorative:

def f2(x:int) -> str:  # these are merely annotations
  ...

# exactly the same as
def f2(x):

Mutable types as default parameters have a crazy rule - if you change them, they stay changed. That's completely nuts. This function adds the length of a list to its end, supplying an empty list as a default. Each time it's called using that default, it permanently grows:

def AA(L=[]): # default empty list -- so far!
    L.append(len(L))  # add length to end
    return L

AA( [5,5,5] )  # [5,5,5,3] normal call, all is fine	    
AA(); # [0], used default, added the 0. so far so good

AA()  # [0,1]  yikes! default was [0]
AA()  # [0,1,2] default was [0, 1], which grew to [0,1,2]

AA(["cat", "dog"])  # ["cat", "dog", 2]  # this works
AA()  # [0,1,2,3] -- but default is still growing!

I suppose it's a quick way to give a function a persistant private variable, or else a bug you should never exploit.

Function pointers, anon, closures

Function pointers are simple. since you don't need to declare the type. Just assign the function name to a variable:

f1 = max
n=f1(3,6) # 6  no special syntax to use it - just a normal call

Anonymous functions are limited. They're allowed a single expression which is implicitly returned. To shorten them, the parameters use no parens:

f1 = lambda x,y: x+y
f1(4,7) # 11

min3 = lambda a,b,c: min(min(a,b),c)

Functions are 1st-class objects. They can be passed or returned. This returns either the built-in min or max function, or, just to show we can, some crazy lambda function:

def getComparer(ch):
  if ch=="animal": return min
  elif ch=="mineral": return max
  else: return lambda x,y: x*x<y  # very contrived use of anon func
  
cc=getComparer("mineral");  cc(5,8)  # 8, since cc=max

Functions can have nested functions. That allows helper functions to be hidden, but it also allows them to be returned with closures. Here's the boring use, bestCat using helper isBadCat:

def bestCat(c1, c2):
  # nested helper function:
  def isBadCat(c): return c.lower() in ["boomer", "zubb"]
  # the 2 bad cats
  
  # handle cases where one or more are bad:
  b1=isBadCat(c1); b2=isBadCat(c2)  # using the helper function
  if b1 and b2: return  # None
  if b1: return c2
  if b2: return c1
  
  return c1.age > c2.age

Putting isBadCat inside just made things look a tad nicer. The really fun use is returning inner functions, capturing variables from the main function. Here clampMaker returns its inner clamp_aux function with lower and upper magically saved:

def clampMaker(lower, upper):
  def clamp_aux(n):
    # NOTE:  lower and upper are captured:
    if n<lower: return lower
    if n>upper: return upper
	return n
	
  return clamp_aux

c1 = clampMaker(1,10)
c2 = clampMaker(2,5)
c1(12)  # 10  c1 is set to 1-10
c2(12)  # 5   c2 is set to 2-5

Returned lambda functions also capture variables (but you have to figure out how to compute your result in a single expression).

More list tricks

Misc

x in L is a contains check for any sort of list. It even works for substrings: "sh" in "fishy" is true. But it won't work for sublists: [4,5] in [1,4,5,9] is false since it looks for [4,5] as a single sub-item.

+ adds any sorts of lists end-to-end (it's a shortcut for extend). Multiplying a list (or a string) by an int adds end-to-end copies:

L += [3,9,10]  # adds to end as 3 elements (not 1 list)
L += [n]  # a hack for L.append(n)

A=[1,2]; B=A*10  # [1,2,1,2,1,2 ... ]  # multiply by int
"Abc"*3  # AbcAbcAbc

for loops on dictionaries go through all of the keys, just because. Or using D.items() creates a list of (key,value) tuples:

# for-loop on a dictionary iterates the keys:
for key in D: 
  val = D[key]
 
# or avoid 1 step and use D.items() to get key+val at once:
for key,val in D.items():

slicing

As with any modern scripting language, Python supports slicing, including negative #'s to start from the right and missing indexes meaning "to the end". The syntax is A[start:onePastEnd]:

w="catfish";
w[2,5]  # "tfi" -- items 2,3, and 4
w[:3]  # "cat" -- everything up to item 2
w[2:]  # "tfish" -- item 2 and beyond

w[:-1]  # "catfis" -- all but last item
w[-2:]  # "sh" -- last 2 items

Slices are new list objects (some Python-like languages make them iterators over part of the original list, but not Python).

Overall list functions

Python has the usual operations on lists. Nothing fancy. zip combines several lists (up to the shortest length) into a single list of tuples:

C=zip([1,2,3],["a","b","c"]) # [(1,"a"),(2,"b"),(3,"c")]

map creates a new list with an operation done to every element. Reduce combines every element into a single result:

# make a new list with everything upper-cased:
L2 = map(lambda w: w.upper(),  ["cow", "Arf", "RatTat"])

# make a sentence out of words in a list:
S=["the", "lonely", "fish"]
import functools  # reduce is here
addWords = lambda w1, w2: w1+" "+w2
n = functools.reduce(addWords, S)  # "the lonely fish"

The more serious use of reduce is when the answer isn't the same type as the elements. Then you need to give a starting value and know that it goes left to right. This adds the length of every word in a list:

# add length of new word to the previous sum:
lengthSum = lambda old, newword: old+len(newword)
n = functools.reduce(lengthSum, ["cat", "dog", "turtle"], 0)  # 12

These are for people who really enjoy built-ins. A regular loop would do this about as well.

Special list loops

Pythoners really enjoy special list and dictionary-making loops. List symbols around the whole thing emphasizes how you're creating a list. The first item represents the element to repeatedly add. The rest is a mostly-standard foreach loop. This makes a list with five 3's:

[3 for num in range(5)]  # [3,3,3,3,3]

Math is allowed in the element part. We're also allowed a single IF afterwards. Here's a complicated way to make [20, 40, 60, 80]:

[x*10 for x in range(1,9) if x%2==0]

We can make a filter with it. This makes a copy of L with no negative numbers:

# remove negative numbers:
L2=[n for n in L if n>=0]

There's no way to add extra items. For example [a,b for ... ] to turn each element into 2, won't work at all. At best you can create tuples or sublists: [(a,b) for ...].

Dictionaries can be created. Put {} around the whole thing and use naked key:val as the element. This turns a list into a dictionary with 0's for values:

{ w:0 for w in Animals}  # ["cat", "dog"] into {"cat":0, "dog":0}

Inner loops are allowed. This seems excessive, but some people really enjoy this style. As usual, rightmost loops are more inner. The first loop below makes N copies of each number N. The second writes each letter in each word, skipping words beginning with "x" and all spaces. Notice how the ch comes from the final loop:

[n for n in L for i in range(n)]  # [2,4] becomes [2,2,4,4,4,4]
# ex: if n is 4, the inner loop runs 4 times, making 4 4's

# this one is ugly: all words not starting with x, all letters except space:
[ch for word in W if len(word)!=0 and word[0]!='x' for ch in word if ch!=' ']

# ["a cow", "xort", "   Q"] to ["a","c","o","w","Q"]

This shows off about the worst part of Python: sure, it's mostly minimalistic, with the standard shortcuts. But it goes nuts for stuff that seems cool.

Sets

Items inside curly-braces, without the colon-pairs, makes a set. They use membership, union and intersection are in, | and &. Sets don't allow [] look-ups:

A={1,2,3};  B={1,3,4}  # sets
C=A|B # {1,2,3,4}
C=A&B # {1,3}
if 2 in A:  # True

A.add(8);  A.remove(8)  # the usual

Regular expressions

Python has the usual regular expressions. re.search searches and returns a match object. A match casts to true/false and also tells you what and where it matched:

from re import search

# looks for a money amount ( $3.67 or $198.00, but not $.15 or $1.6):
# Notes: \d is a digit, \. is an escaped dot, \$ is an escaped dollar-sign
pattern = ""\$[\d]+\.\d\d"
match1 = search(pattern, "$12.5$.78$1.234")  # finds "$1.23"

if match1:
  text=match1.group(0)
  start=match1.start(0);  end=match1.end(0)
  
  w="found {} from {} to just before {}"
  print(w.format(text, start, end))
  # found $1.23 from 9 to just before 14

Reg-exps's have all the usual: .* for 0 or more of anything, [a|e|i|o|u]{2,4} for 2 to 4 vowels. Adding ? switches from a maximal to a minimal match: "X.*X?" gets the first thing surrounded by X's. It's even got groups with parens and \0, \1 to match a previous group. re.finditer is the same as search, but returns a list of everything matching your pattern.

classes

basic class

Python classes have two odd parts. Member variables aren't declared (since no Python variables are declared), and member functions are all fake (the same way javascript does it). A sample cow class showing these. __init__ is the constructor (with double-underscores):

class Cow:
  # no variables are declared here, since vars aren't declared

  def __init__(self, w="", age=0): # the constructor
    self.name=w
    self.age=7

  def describe(self):  # called like: p.describe()
    return self.name + ", " + str(self.age) + " yrs. old"
	
  def addToAge(self, yrs):  # called like: p.addToAge(3)
    self.age += yrs

Every member function must have a reference to "itself" as the first input, which will automatically be sent -- calling c1.addToAge(3) actually runs addToAge(c1,3). This is because member functions are fake. They're just normal functions and you have to tell them which cow to use. They have use self.name and self.age to find that cow's name and age.

There's nothing special about self. It's just a variable name -- you could use s or theCow. But self is nice since people are used to it and if feels official.

The way you figure out a class's member variables is by seeing what the constructor creates. Anyone could add more (c1.milk=6 is legal anywhere and creates a new milk member variable), but it's better to agree that only the constructor will.

There's no new. Constructors are called like:

c1=Cow()
c2=Cow("bessy")
c3=Cow("lu-lu",4)

There's no public or private. Everything is automatically public. By convention variables starting with an underscore should be treated as private: _id, _age_in_days, and so on.

Python has static class variables (one copy for the entire class). They're declared directly in the class:

class Frog:
  # these are static class variables:
  bestType="spotted croaker"
  count=0

Use them the normal way, with the classname in front:

ft = Frog.bestType 
Frog.count+=1

Python also has Static class functions, which are double weird. First they need @classmethod on the previous line. Second the class calling them is automatically the first parameter:

class Goat:
  count=0
  
  @classmethod
  def canEat(g, foodName):  # g will always be class Goat
    return True  # goats eat anything

We call it with the class name, like Goat.canEat("tin cans"). That's turned into canEat(Goat, "tin cans"). We need the class name since we might need to look-up a class static (aarrrg!):

class Goat:
  count=0  # hasGoatArmy will need this
  
  @classmethod
  def hasGoatArmy(g):
    return g.count >= 100

And again, this is technically a normal function, with no special access to Goat. That's why g.count is required to access count.

Sadly, the constructor, __init__, can't be overloaded. Instead use static functions as alternate constructors. Here makeHopper creates an alternate type of Frog:

class Frog:
  def __init__(self, name="frog"): self.name=name 

  # this is an alternate constructor:
  @classmethod
  def makeHopper(frogClass, jumpHt):
    theFrog = Frog()  # borrow the constructor
    theFrog.hopHt=10
    return theFrog	

f1 = Frog()
f2 = Frogs.makeHopper(4)  # an alternate "constructor"

One thing about static variables is just nuts. Instances can use them, but can't change them -- assigning creates a new member variable. It's a confusing mess:

Frog.count=9  # correct way to use a static

f1 = Frog()
f1.count  # 9 !! This shouldn't work, but it does

Frog.count=27
f1.count  # 27 !! It's really tracking the class count

f1.count+=2
f1.count  # 29  seems to work, but...
Frog.count  # 27  ...Nooo!
# f1 accidentally made a personal count variable

It's pretty much a trap waiting to happen.

Polymorphism

Python is super maximally polymorphic. It has to be since we never know types in advance. Each time our running code sees a+b we have no choice but to look up the types an decide which + to run, or else through an error. Im C++ terms, every function is already virtual.

This means Python has no need for interfaces -- they ensure compile-time safety, and Python scoffs at safety. Likewise inheritance and base-classes mean nothing here. If classes A and B have the functionality required by myFunc that's good enough -- no need to invent a common base class (and to repeat myself, there's also no way have myFunc require that as input).

Inheritance

Again, in Python we don't need inheritance for polymorphism; but it's there if we simply want to inherit stuff. After the class name add the base class name in parens. The base class constructors are called like: baseClassName.__init__. Here Cow inherits from Animal:

class Cow(Animal):
  def __init__(self, name="", age=1, milk=0):
    Animal.__init__(self, name, age)  # calling base constructor
    self.milk=milk
    
  def sellPrice(self):
    basePrice=Animal.sellPrice(self)  # calling base class function
    return basePrice*2

Multiple inheritance is allowed:

class RoboCow(Animal, Robot):  # multiple inheritance

Of course you'll need to manually call Animal.__init and Robot.__init__.

What do we actually inherit? Well, functions are stored with the class type, so we inherit all of them. We don't actually inherit variables. But if we call our base class constructor, it creates them, so effectively we inherit variables, too.

operators, casts

Python classes can define operators and casts. The special names are words surrounded by double-underscores (the same idea as __init__). Here's a typical string cast for Cow:

  def __str__(self):
    return "{}, {} years old".format(self.name, self.age)

Now str(c1) works.

+ and - can be overloaded using __add__ and __sub__. Another fun one is __call__ which overloads ()'s. Here they are for a Pair x,y class:

class Pair:
  def __init__(self, x=0, y=0):
   self.x, self.y = x,y  # using multiple assign trick
      
  // overloading Pair+Pair:
  def __add__(self, other):
    # add x's and y's of each to get new Pair:
    return Pair(self.x+other.x, self.y+other.y)
  
  def __call__(self): print("function call")

p3=p1+p2  # runs the __add__ function
p3()  # runs __call__, which is a bit silly in this case  

Properties

No scripting language can resist the lure of Properties -- functions that look like variables. In Python you list them 1-at-a-time with a decorator (@property means get):

class Cat:
  # assume pnds is the cats's weight in pounds
  
  @property
  def kilos(self): return self.pnds/2.2
  @kilos.setter
  def kilos(self, k): self.pnds=k*2.2

Notice how they have the same property name. It knows which is which because of the @'s. Also notice how they're both normal functions. Setter is optional (plus another optional deleter).

This is a shortcut for the real way which no-one uses. You write kilos=property(getFunc, setFunc, delFunc). That creates a property object and assigns it to kilos. The @-macros create this for you.

More on storage

Python's variables are saved in dictionaries. Using a variable is really just a dictionary look-up. If you want, you can use the "all-variables" dictionary directly. globals() gives you the dictionary for the file (the "module"):

# first make some sample variables:
n=2;  c="cow"

# here's where Python keeps them:
globals()  # {'n':2, 'c':"cow" }  plus a lot more system stuff

# how to check if a variable exists:
if not "n1" in globals(): print("no such variable n1");

If you want to delete a variable, a normal pop command from the main dictionary will do that:

globals.pop("n");  # n is no longer a variable

That's right. Python allows you to delete variables, something no other language does. del n1 is a shortcut. Why do this? Maybe to save space?

locals() gives the variables in the current function (not even in the class -- just the current function).

Class member variables aren't technically variables -- they're attributes. Attributes are variables attached to objects, accessed with a dot. They can be listed and examined using vars(object):

# An empty class, for us to play with:
class A: pass

a=A();
a.x=7;  a.name="cow"  # member vars (actually attributes)

# list them:
vars(a)  # {"x":7, "name":"cow"}

You can check for one the normal way: "x" in vars(a), but Python adds shortcuts:

if hasattr(a, 'name'):  # does a.name exist?
n=getattr(a,"name")  # same as: n=a.name
setattr(a,"cost", 1.75)  # same as a.cost=1.75

Why use getattr and setattr when = is so much easier? Sometimes things "feel" better as attributes than as members.

We can delete member variables with that del command:

del a.x  # x is no longer a member var
vars(a).pop("x")  # the long way

  def f(self): del self.x  # in a member function

Every object has a few built-in attributes. __class__ holds the type (type(x) is the same as x.__class__). n.__doc__ holds the doc-string. Functions and classes have __name__ and __module__:

n=5;  n.__class__  # < class 'int'>
n.__doc__  # explains options for int() casts

Frog.__doc__='also used for toads'
Frog.__name__  # 'Frog'
max.__module__  # 'builtins'

These sometimes reach through to the type. For example n.__doc__ is actually showing you int.__doc__.

Deep-down, Python objects only have preset built-in attributes, including one named __dict__. That stores the ones you add. When you write a.n=5 it's actually a.__dict__["n"]=5. vals(a) is really just a shortcut for a.__dict__.

Your functions can also have attributes added. I'm not sure why you'd want to, but you can:

def knightName(w): return "Sir "+w
knightName.count=0  # function knightName has attributes now
knightName.abc="def" #again, abc springs into existence

w1 = knightName("George")
n1 = knightName.count + 2

Fun with strings into variables

Variables being in a dictionary means Python is comfortable converting strings to variables and back. Here we can read a string and turn it into a variable (by setting an attribute with that name):

aName=input("enter atribute name: ")
setattr(a, aName, 10)
# if they entered "size", then a.size is now 10

Or we can search for attributes with certain patterns in their names. Here we search for f1.a through f1.z:

# check f1 for all 1-letter attributes:
for n in range(ord("a"), ord("z")+1):
  attr=chr(n)  # "a" to "z"
  if hasattr(f1, attr):
    print("f1."+attr+" exists")

# possible output:
# f1.n exists
# f1.y exists

We can call a fucntion with a "random" named parameter (read in from just any string) by abusing the ** dictionary trick:

p0=input("enter a parameter name: ")
D={ p0:10 }  # make our string the key of a dictionary
f(**D)  # unwrap it into a parameter

If they entered "cows" that would run function f(cows=10).

Installing (for me)

Everything online about installing Python is a lie. Python is now named py.exe, not Python. The install path you're shown is a lie - it's actually where your installed package data is stored. You'll be told to run a companion program named 'pip' to install more python packages -- also a lie, there hasn't been a program named pip for years. Instead run py.exe -m pip install packageToInstall.