Python for Programmers

Python is a "scripting" language. This means it has contempt for efficient storage: there's only one type for decimals, no char type, and no fixed-size array. There's also no type safety -- any variable can be any type at any time. As a reaction to overcomplicated Perl, Python has a minimal feature set -- for example, there's no switch or do-while. It's also famous for having no curly braces or other block delimiters, instead having indentation.

Variables are generic pointers to typed objects

All variables are generic references to any type. type(n) checks n's current type. Variables aren't declared -- assigning creates them as needed:

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

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

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

Using a variable checks the current type. Essentially, everything is virtual. These use different len functions:

n=[1,4,6]; len(n)  # 3

n="cowbell"; len(n)  # 7 (length of a string)

n=12; len(n)  # run-time err: object of type int has no len()

Function parameters also have no types. Below, the silly function sum runs on anything with a plus operator:

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

# we can run it on anything with a +:
sum(4,5)  # 9
sum("cat","fish")  # catfish

Basics, how to do normal stuff

Types

The basic types are: int, float, str (string), and bool. There's no nonsense with long/short number types. There's no char type -- strings are made of length-1 strings. Strings can use single or double quotes. bool's use capital True/False:

n=5; f=5.7; b1=True  # basic types

w1="camel"; w2='clutch'  # declare str using single or double
ch=w[0];   # "c",  ch is a small str

The only list type is list. Python has no primitive array, and there's no linked-list (the list type is actually array-backed). Indexing works as normal. As a bonus, negative indexes go from the right (N[-1] is the last element, and so on). They're ranged-checked. They have the usual operations, even the slow ones:

Nums = [4, 8, -6, 3];  len(Nums);  Nums[0]=5  # basic list use

x= Nums[-2]  # second to last element: -6
y = Nums[999]  # out-of-range exception

Nums.append(7); Nums.extend( [9,9,9] )  # add to end (element or another list)
Nums.insert(2, 942)  # insert before an index

n=Nums.pop()  # remove from end
Nums.pop(0)  # remove arbitrary index
Nums.remove(942)  # search and remove (1 item)

Nums.index(8)  # search for 8 and return index, or error if not found

Tuples are read-only lists, made with optional parens and commas. They also use len, and are indexed with [] again. The T[-1] reverse-index trick also works:

c1 = "fluffy", 8, 1.4  # the tuple ("fluffy", 8, 1.4) 
type(c1)  # <class 'tuple'>,  again, notice absence of length and element type
c1[0]; c1[-1]  # "fluffy"  1.4 (last item)

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

if, loop

If statements don't use parens, have the usual compare operators, and end in a full colon. Boolean logic is lower-case AND, OR, and NOT. There's nothing to delimit the block, only indentation:

if n>=0 and n<=10: print("0-10")

if not(n>=0 and n<=10):
  print("not 0-10")
  print("try again")

else's also end in a colon. Oddly, else-if is an error. Use the compound elif:

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

Loops are a basic while, and a foreach. There's no "normal" for-loop:

x=1;
while x<100:
  # do stuff with x
  
# for is a foreach-style loop. This examines a list:
for x in [3,6,9,12]
  print(x)

# another, looping through a string to remove all a's:
w2 = ""
for w in "camel":
  if not w=='a':
    w2+=w

Typical counting for-loops can be faked using the range function:

for n in range(10): print(n)  # 0 through 9

functions

Functions start with def and end with a full colon. They don't say whether they return anything. They're actually allowed to return inconsistent types, or nothing (which is a terrible idea). The most interesting thing is how they run on any types that will let them:

def max(n1, n2):
  if n1>n2: return n1
  return n2
  
max(5,8)
max('cat', 'fish')  # "fish" -- max works on any type with a >

format

Ending statements with semicolons is optional, commonly only done between statements on the same line. Indentation can be any amount, but must be the same within a block. To indicate an empty block, use pass:

if b>c: 
  b=0  # we choose a small indentation
  c=99
  if(d>10):
         d=0  # indented more here, since we felt like it
         c=999
  e=0 # same indent as above, which is required
  
def doNothing():
  pass  # <-- the official Python "do nothing" placeholder ( since it can't use {} )

casting

Casting is the usual: str(43) is "43", int(4.9) is 4, int("12") is 12. Fun fact: n=2; n+=0.1 changes n from an int to a float. To look for illegal casts, which throw errors, we have isdigit for ints, and try/catch for floats:

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

try: n=float(w)
except ValueError: n=-999  # testing: float("abc") gives a ValueError

Most list-like types cast back and forth:

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]

Strings combine with a simple +. There's no implicit cast from int or float. But format will do it, replacing {}'s with inputs:

w = "Weight is " + str(n) + " pounds"  # no implicit cast to str

w= "The {} is in barn number {}".format("cow", 15)  # cast and replace in order
w= "Or fill {0} in using {1} for each {0}".format("slots","numbers")  # select which parm

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. Python uses the terms "mutable" (Reference type) and "non-mutable" (Value type):

# ints act as value-types:
n1=6; n2=n1; n1=0 # n2 is still 6

# lists (and classes) are pointers:
n1 = [1,2,3]; n2=n1
n1.append(8)
print(n2)  # [1,2,3,8], n1 and n2 share one list

== is a value-compare, is checks pointer equality:

N1 = [1,2,3];  N2 = [1,2,3]

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

Python's null is None. It's the only value of type NoneType. Assigning n=None changes the type of n to NoneType. if x==None will probably work, but the preferred test is if x is None:. It works because all None variables are pointing to a single pre-made instance of None.

Fun fact: n1 is n2 with ints is true for small values and probably false for large ints. That's because, the same as Java, Python pre-makes and re-uses small ints (larger ones are created on-the-fly. n1=542 ; n=542 probably creates 2 instances, but n1=542; n2=n1 shares one).

Basic operations

/ is real division, even with ints. // is integer division (drops the remainder, even with floats). % is modulo, with int and float:

9/2    # 4.5  always real division
9.1//2   # 4.0  // always integer division

8.5 % 2.1  # 0.1, modulo works with ints and floats

Python has n+=1 and so on, but there's no ++ operator. For fun, strings get a multiply: "abc"*3 is abcabcabc.

More tuples and Multiple assign

Multi-assign can be used on any R-value with a len:

N = [5,8,"cow"];  x,y,z = N
# same as: x=N[0]; y=N[1]; z=N[2]

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

a,b,c = d,e,f
x,y = y,x  # a swap!!

Technically the left is a tuple. a,b,c=N is actually (a,b,c)=N.

We can assign from too-big lists using the underscore as a "dont-care" variable, and a leading star as a glob (a wildcard) matching 0 or more items:

x, y, _, _ = N  # first 2 items, N must be length 4
x, y, *_ = N  #  first 2 items, N is any length 2+

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

If the globbed item is a variable, it gets the combined items as a list, an empty list if need be:

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

You're allowed to dig into the structure. This gets the first 2 letters of the first word:

(ch1, ch2,  *_), *_ = ["monkey", 6, 12.3]
# ch1="m", ch2="o"

Functions can return tuples to allow multi-assign on the calls. This longish function converts strings like "12,8" into a pair of ints:

def parseStrPointIntoIntPair(p):
  parts = wPoint.split(',')  # parts is the list ["12","8"]
  x,y = map(int, parts)  # applies the "int(n)" cast to each element
  return x,y  # returning 2 values as a tuple
  
p1 = parsePoint("23,58")  # p1 is the tuple (23,58) 
n1,n2 = parsePoint("23,58") # n1 is 23, n2 is 58

importing modules

Python's namespaces are files (ending in py). It calls them modules. import fileName "activates" that file. The full path is required: fileName.someFunc. To avoid that, an extra from can import specific items:

# simple module inclusion:
import random
random.randInt(10,20) # the path is needed

# use FROM to import specific functions:
from random import randint, uniform 
randInt(10,20)  # no path needed 
uniform(2,3)  # (a float between 2 and 3)

No special wording is required in a python file. Items are just there "naked". This file makes the module cats:

== cats.py: ===
totalCatCount=0  # a global variable

def isCatName(w): return w.lower() == "fluffy"

We can then use import cats or from cats import isCatName. But the file needs to be either in the current directory, or somewhere python can find. To create a searchable directory tree we need to create a "package". Doing that is weird: create an empty file named __init__.py in each directory and subdirectory. That allows you to follow a path with dots: import dirName1.dirName2.fileName.

But you may still need to put the first directory in the list of what Python can find. Shell-script-style, sys.path contains locations, in a list. We can import it and add the one we want:

import sys
sys.path +=["/Users/bob/myPythonFiles"]
# assume animals in in myPythonFiles, and cat is in subDir mammels:
import animals.mammels.cat

Whew!

No block scope

Python does not have block scope. It has file (module) scope and function scope. In this code, temp is created on-the-fly, and can be seen outside of the IF, unless x is 10 or more, in which case temp is never created:

if x<10:
  temp=8

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

This isn't as bad as it looks, since you can freely repurpose old variables. Accidentally re-using int i for a string here is no problem:

for i in [4,6,7,12]: print(i)

# i is currently 12 (but we don't care). Re-use it for this str loop:

for i in "rocket": print(i)
# i is currently "t" (but again, we don't care)

More on loops

The for loop will go over a list, tuple, string, or iterator:

for ch in "horse": print(ch)  # each letter

for x in 3,4,6,7:  # using the tuple shortcut

The range function for counting loops comes in lots of flavors:

for x in range(10): # 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)

To also see indexes, a special enumerate function will zip-up a list with a sequence of numbers, in 2-tuples (the order flips: the sequence is last in the call, first in the result):

# enumerate zips a list with a sequence:
A2 = enumerate(A,-10) # an iterator
list(A2)  #  [(-10,a0), (-9,a1) (-8,a2) ... ]

# actual use:
for i,n in enumerate(A,0):
  print( "index is {}, item is {}".format(i,n)

Notice we're using the multiple assign trick. All those tricks work in a loop. This will read the second letter of each word (and crashes on length 1 words):

for _, ch, *_ in "cat","dog","squirrell":  # a, o, q

There's a special-case loop else for when you're expected to use a break to quit when you're happy. The else runs if you finish the loop. If means "not found" or more precisely "if we didn't break":

for x in range(len(A)):  # another way to run 0...len-1
  if A[x]>=1:
    print('found a positive number at'+str(x))
    break
else: # this only runs if the loop didn't use break to exit
  print("no positive values in list")

More on Functions

Fnction parameter types & return types may optionally be written but it's only a comment and not enforced:

def funcWTypedParms(x:int) -> int :  # note: these are comments
  return x+3
  
funcWTypedParms(10.2)  # 13.2 -- the int isn't enforced

Default parameters may be added using the usual = after the name:

def diff(x, y=0): print(x-y)

diff(6); diff(9,2) # 6 7

In the call, you're allowed to supply parameter names. Syntax is a little different: it's name=value.

def sum(a,b=10,c=100): return (a+b)/c  # a sample function

sum(6,c=5) # passing c by name, b gets default value of 10
sum(b=1,c=2,a=3) # mixing up the order

Mutable types as default parameters have a crazy rule - their values persist across calls. This is completely nuts:

def AA(L=[]): # default array parm is initialized to empty: []
    L.append(0) # this can permanently modify the default parm
    return L
	    
AA(); # [0], so far so good
AA(); AA() # [0,0] [0,0,0]  default value of L keeps growing

AA([1,2,3]); AA([6]);  AA([2,2,2])  # no change
AA()  # [0,0,0,0] -- once again we're mutating the default parm

A list can be unrolled into individual parameters for a function call, using a star:

def takesTwoParms(x,y): ...

A=["cat", "dog"]
takesTwoParms(*A)  # same as takesTwoParms(A[0], A[1])

Function pointers are simple. since you don't need to declare the type:

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

Anonymous functions are limited and use shortcuts: no parens, and implicit return. They can't have any statements, but you can fake it by using a real function:

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

def saySize(x):
  if x>=100: return "big"
  return "small"
  
f1=lambda x: saySize(abs(x))

Functions are 1st-class objects. They can be returned or curried. Here clampMaker returns a function which clamps to the range we give it:

def clampMaker(lower, upper):
  return lambda x: min(upper,max(x,lower))  # yes, this is a clamp
  
f1=clampMaker(2,5); f1(0)  # f1 now clamps in range 2-5

This basic curry creates a new version of addToAll taking the 2 inputs 1-at-a-time:

# convert addToAll into taking 1 input at a time:
addToAllCurried=lambda x: lambda L: addToAll(L,x)

add5=addToAllCurried(5); add5(A)

More list tricks

x in L is a contains check:

if " " in w:  # does w have a space

if 5 in L:  # L can be either a list or tuple

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

a2=[1,2,3]+[11,12]  # [1,2,3,11,12]

A=[1,2]; B=A*10  # [1,2,1,2,1,2 ... ]

A hack to append 1 item is to make it into a list then add it with +: A+=[5].

Slices use the syntax A[start:onePastEnd]. For example A[4:6] is two elements: 4 and 5. Leaving one out defaults to the corresponding end:

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

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

zip combines several equal-length lists (or the shortest length) into a list of tuples:

C=zip([1,2,3],[4,5,6]) # [(1,4),(2,5),(3,6)]

Python has list operations "apply to all" (map), and reduce (reduce is the one that combines all pairs in the list to create a single value, for example, adding them together):

L2 = map(lambda w: w.upper(),  ["cow", "Arf", "RatTat"])
list(L2)  # same list, but all are upper-cased

import funcTools
combineWithAnd = lambda w1, w2: "{} and {}".format(w1,w2)
L2 = funcTools.reduce(combineWithAnd, L)
list(L2)
# "1 and 5 and 8 and -6"

Set, Dictionary

Items inside curly-braces make a set. They use in OR(|) and AND(&). They 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}
has2 = 2 in A  # True

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

Dictionaries also use {}, with the key:value syntax. Or they can cast from anything containing pairs:

 A={"cat":6, "dog":8}  # a dictionary
 
 "cat" in A  # True
 A['cat'] # 6  # lookup with a key
 A['frog']=9  # add or change an element the usual way
 
 A1 = [("cat",6),("dog",8)]  # a list of tuples
 D1 = dict(A1)  # a dictionary

Dictionaries have a for shortcut to get key and values together:

# dict loop w/o a shortcut:
for key in D: # a normal for reads only the keys
  val = D[key]
  
for key,val in D.items():  # items() shortcut gives (key,value) pairs

More on storage

Python's system for variables is merely an {identifier:object} look-up (a dictionary) for each scope. You're allowed to search it or remove items. They're named locals() and globals():

n=2;  c="cow";
locals()  # {'n':2, 'c':"cow" }
type(locals())  # dict

if not "n1" in locals(): print("no such variable n1");

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

locals()['c']="horse"  # same as: c="horse"

Checking for an member variable (called an attribute) uses: hasattr(c1, "varName").

Regular expressions

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

from re import search

# looks for a money amount: $, at least one digit, a dot, 2 digits:
# Notes: \d is a digit, \. is an escaped dot, \$ is an escaped dollar-sign
match1 = search("\$[\d]+\.\d\d",  "$12.5$.78$1.234")

if match1:
  print("got a match")
  w="matched {} from {} to just before {}"
  print(w.format( match1.group(0), match1.start(0), match1.end(0) ))
  # matched $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 groups. re.finditer is the same as search, but returns a list of all matchObjects.

classes

Since we can't declare member variables, Python classes "declare" them javascript-style, by assigning to them in the constructor. This is only a convention. __init__ has no special privileges. Like the rest of python, at any time c1.n=6 creates n inside of c1. The constructor is named __init__ (double underscores):

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

  def __init__(self, w="", age=0): # a constructor
    # "declares" name and age, sort of, by initializing them:
    self.name=w 
    self.age=7  # the leading self-dot is required, otherwise we use global age

There's no new: just c1=cow("bessy"). You're only allowed a single constructor. If you need more, Python encourages use of static class functions (below).

All member functions use that funny "first parameter is calling object" rule. That's because member functions are faked in Python. c1.func1(5) is converted into normal function: cow.func1(c1,5). Inside, the function has no special access to its member vars -- it must go through the object. Two cow member functions:

  def addToAge(self, howMuchOlder):
    self.age+=howMuchOlder
  
c1.addToAge(4);  # call as normal, but becomes: cow.addToAge(c1, 4)
  
  def toStr(me):  # <-- self is the convention, but not a keyword
    return "name: {}, {} yrs old".format( me.name, me.age )
     
c1.toStr();  cow.toStr(c1)  # these are the same

Notice how there's no public or private. Python has no private. By convention, variables starting with an underscore should be treated as private.

Static class variables are declared directly in the class. Static class functions need the @classmethod tag. They use the same hack as member functions: the class calling them is an automatic first parameter (in this case, Frogs):

# a class with a static variable and a static function:
class Frog:
  bestFrog="spotted croaker"  # class variable
  
  @classmethod
  def isBestFrog(fc, frogName):  # fc will always be "Frog"
    return frogName == fc.bestFrog
 
 # use them in the usual way:
Frog.bestFrog="striped croaker"
if Frog.isBestFrog("red hopper"):

Notice how the fc variable points to a class, not an instance. Python allows variables to aim at types: x=type("abc"); y=str is fine. x(45) casts to "45". This trick (sort of) is used with static creation functions:

  @classmethod
  def makeHopper(frogClass, jumpHt):
    theFrog = frogClass()  # this will be running: Frog()
    theFrog.hopHt=10 

fh1 = Frogs.makeHopper(4)  # an alternate "constructor" from Frog

Inheritance uses parens around the base class. Multiple inheritance uses commas. But recall you don't need inheritance to be polymorphic in Python. Base class constructors are called the usual way (baseClass.__init__):

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

Class casts and operators are done like init: double-underscores surrounding a magic word. Casts use the type name, add and sub overload + and -, and so on:

class Pair:
  def __init__(self, x=0, y=0):
   self.x, self.y = x,y
    
  def __str__(self): return "{}, {}".format(self.x, self.y)
  
  // overloading Pair+Pair:
  def __add__(self, other): return Pair(self.x+other.x, self.y+other.y)
  
p1 = Pair(3,4);  str(p1)  # "3, 4"
p2 = Pair(10,20);  str(p1+p2)  # "13, 24"