Python Notes

CSCI 4511 Spring 2025

These notes are meant to accompany a lecture in the course. If you are looking for a full Python reference, I recommend the full Python reference.

Python From Java

Whitespace Syntax

Python formally uses whitespace as part of its syntax. Examples:

You may be used to languages ending logical lines with a semicolon: ;. Python ends logical lines with a carriage return (a new line).

Compare Java:

System.out.println("Hello");
System.out.println("World");

with Python:

print("Hello")
print("World")

You can extend a logical line of code over multiple lines using backslash characters:

print("Hello \
")
print("World")

Logical lines can be extended inside of groupings such as arrays:

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

Where languages like Java use curly braces { and } to group statements, Python typically uses indentation.

Compare Java:

for (int i = 0; i < 5; i++) {
  System.out.println(i);
}

with Python:

for i in range(5):
    print(i) # note that the print statement is indented 

The above also demonstrates:

  • Comments are indicated with #
  • for loop syntax is concise
    • Declaration of i and incrementing i += 1 are implied
    • Python does not have ++, use +=1
  • Things that are associated with groups of statements end in a colon :

Nesting loops and/or conditionals uses multiple levels of indentation:

for i in range(3):
    for j in range(4):
        print(i+j)
        print(i*j)

The Interpreter

You can run the Python interpreter from the command line and experiment with code. Open a terminal and simply run python.

Operators

Basic math operators work similarly in Python and Java:

print(1 + 2)
3
print(1 * 2)
2
print(1 / 2)
0.5

Dividing two integers (or any two numbers) with / automatically yields a float. Quotient/remainder is available with the quotient // and remainder % operators:

print(7 // 2)
3
print(7 % 2)
1

These are not constrained to integers:

print(6 // 2.5)
2.0
print(6 % 2.5)
1.0

Operators like + and * work like math operators when the things on both sides of them are numbers. They do other things when they’re around strings.

+ concatenates:

print("Fizz" + "Buzz")
FizzBuzz

* extends the string, when used with an integer:

print("Fizz" * 5)
FizzFizzFizzFizzFizz

The above ‘multiplication’ operation would be nonsensical with a float instead of an int, or with two strings, and trying it will give you an error.

Powers can be raised with **:

print(2**3)
8

There is also a well-documented math library for things like square roots.

import math

print(math.sqrt(2))
1.4142135623730951

Note: There is nothing wrong with the math library, but most people use numpy for math these days.

import numpy as np

print(np.sqrt(2))
1.4142135623730951

Conditionals

Conditionals are expressed with if, elif, and else:

  • The if is required.
  • Any whole number of elifs are allowed.
  • Zero or one elses are allowed, and the else always comes last.

Syntax uses colons and indentation, just like loops.

x = 5

if x > 10:
    print("x is huge")
elif x < 0:
    print("x is negative")
elif x > 0:
    print("x can be counted on fingers")
else:
    print("x is zero")
x can be counted on fingers

(This is a very silly example.)

There is an extremely powerful match case conditional syntax which you can use.1 Read about it here

1 If you are extremely powerful.

Types and Variables

Python has types, but variables do not have types at compile time. Compared to Java, this might seem confusing. You can think of variables as pointing to objects that have types (Python also does not, explicitly, have pointers):

x = 126
y = "sunset"
z = 12.6

Variables can be reassigned without regard to the type the variable is pointing at:

x = 126
x = "sunrise"

Assigning a variable to another variable assigns it to wherever the other variable is pointing:

x = 126
y = "sunset"
x = y
print(x)
sunset

Reassigning the other variable doesn’t change the first variable:

x = 126
y = "sunset"
x = y
y = "sunrise"
print(x)
sunset

Lists

Lists are delimited with square brackets [ and ].

x = [5, 6, 7]
print(x)
[5, 6, 7]

They are not typed and can contain whatever you want. Or, for the imaginative, they can contain any type of object.

x = [126, "sunset", 12.6]
print(x)
[126, 'sunset', 12.6]

Individual list elements can be accessed via “slicing.” Note that lists are 0-indexed.

x = [126, "sunset", 12.6]
print(x[1])
sunset

Individual elements of a list can be modified:

x = [126, "sunset", 12.6]
x[1] = "sunrise"
print(x)
[126, 'sunrise', 12.6]

Lists are collections of references to variables and behave differently with respect to assignment. Specifically, assigning a variable to another variable that references a list will cause both variables to reference the same list object:

x = [126, "sunset", 12.6]
y = x
print(y)
[126, 'sunset', 12.6]

Changing x also changes y, since they reference the same thing:

x = [126, "sunset", 12.6]
y = x
x[1] = "sunrise"
print(y)
[126, 'sunrise', 12.6]

Slicing can access more than one element:

x = [126, "sunset", 12.6]
print(x[1:3])
['sunset', 12.6]

Strings can also be sliced:

x = "sunrise"
print(x[0:3])
sun

More details about slicing in the docs.

Lists can be concatenated with +, which returns a new list:

x = [1, 2, 3] + [4, 5, 6]
print(x)
[1, 2, 3, 4, 5, 6]

Lists have no fixed length, they can be lengthened in place with append()

x = ["sunrise"]
x.append("sunset")
print(x)
['sunrise', 'sunset']

Tuples

Tuples are similar to lists, but they are immutable. They are optionally delimited with parentheses ( and )

x = ("sunrise", "sunset")
print(x[1])
y = "duck", "teal"
print(y[0])
sunset
duck

While the parentheses are optional, they are typically used.

Tuples can also be used for multiple assignment:

A, B = 1, 2
print(A)
print(B)
1
2

Dicts

Hash tables in Python are called dictionaries, or “dicts.” They consist of key-value pairs. They are delimited with curly braces { and }.

This dict has one pair: the key is 'sunrise' (a string) and the value is 126 (an int).

x = {'sunrise': 126}
print(x)
{'sunrise': 126}
  • Keys and values can be any variable type. Values can be lists or dicts, but keys cannot (since these cannot be hashed.)

Dict values can be accessed via their keys:

x = {'sunrise': 126}
print(x['sunrise'])
126

Dict values can also be changed or added via their keys:

x = {'sunrise': 126}
x['sunrise'] = 127
x['sunset'] = 12.6
print(x)
{'sunrise': 127, 'sunset': 12.6}

Looping

Python has several ways to loop through things.

Via index:

L = ["cat", "dog", "owl"]
for j in range(3):
    print(L[j], end=" - ")
cat - dog - owl - 

Via content:

L = ["cat", "dog", "owl"]
for j in L:
    print(j, end= " - ")
cat - dog - owl - 

enumerate 😌

L = ["cat", "dog", "owl"]
for j, item in enumerate(L):
    print(item, j, end=" - ")
cat 0 - dog 1 - owl 2 - 

Looping directly through a dict iterates over keys:2

2 Note the f-string

A = {"carl": 5, "otis": 6, "alan": 2}
for j in A:
    print(f"{j}:{A[j]}", end=" - ")
carl:5 - otis:6 - alan:2 - 

You can also iterate over the values:

A = {"carl": 5, "otis": 6, "alan": 2}
for j in A.values():
    print(j, end=" - ")
5 - 6 - 2 - 

Or the items:

A = {"carl": 5, "otis": 6, "alan": 2}
for i, j in A.items():
    print(i, j, end=" - ")
carl 5 - otis 6 - alan 2 - 

There are while loops:

L = ["cat", "dog", "hoss", "owl"]
i = 0
while len(L[i]) == 3:
    i += 1
print(L[i])
hoss

Booleans

Python uses and, or and not for boolean logic. Equality operators are the same as Java (==, <, !=, etc.). Parentheses aren’t enforced, but it’s a fine idea to use them anyway.

Bools can take on either True and False.

print(1 != 2)
print(4 <= 3)
print(0.1 + 0.2 == 0.3)
True
False
False

Boolean operators short-circuit, which means:

  • If the left side of an and is False, the right side isn’t evaluated.
  • If the left side of an or is True, the right side isn’t evaluated.
print((1 > 0) or nonsense)
True

nonsense above isn’t defined but the reference is never reached, so the program runs without error. Short-circuiting is occasionally useful and occasionally gets you into trouble.

Truth

Python has “truth values” for most built-in types. 0, empty values, and None (null) evaluate as false, non-zero and non-empty values evaluate as true, even though they aren’t equal to the boolean.

x = []
if not x:
    print(x == False)
    x.append(1)
print(x)
False
[1]

Functions

Functions are defined with def and a colon. Indentation to group statements follows the same conventions you have already seen.

def doubling(x):
    return x*2

y = doubling(2)
print(y)
4

Functions do not have to return anything.

def good_morning():
    print("sunrise")

good_morning()
sunrise

Variables passed to a function are copied into the function and not modified outside of the scope of the function.

def add_one(x):
    x += 1
    return x

y = 2
z = add_one(y)
print(y)
2

Lists passed to a function are passed as references, and are modified outside of the scope of the function.

def append_sunrise(x):
    x.append("sunrise")
    return x

y = ['sunset']
z = append_sunrise(y)
print(y)
['sunset', 'sunrise']

This is a decent time to note that you can get into some trouble with functions because nothing in Python is explicitly typed:

def append_sunrise(x):
    x.append("sunrise")
    return x

y = 'sunset'
z = append_sunrise(y)
print(y)

OOPython

Modules and Imports

Virtual Environments

You probably know this:

import numpy

print(numpy.random.random())
0.8741313519853512

and this3

3 It might not run if you don’t have scipy installed, so install scipy.

import numpy as np
import pandas as pd
from scipy import stats

a = stats.uniform(1, 5)
print(a)
print(a.cdf(2), a.cdf(3), a.cdf(5))
<scipy.stats._distn_infrastructure.rv_continuous_frozen object at 0x115db3920>
0.2 0.4 0.8

Where do imports come from? The environment. There’s a default environment; but for any complicated project, you’ll want to create your own. You can call it whatever you want. .venv works. You could call it otis. You should call it something meaningful.

At the command line:4

4 Your install might use python3 instead of python

python -m venv otis
cd otis; tree | head -n 172
.
├── bin
│   ├── activate
│   ├── activate.csh
│   ├── activate.fish
│   ├── Activate.ps1
│   ├── pip
│   ├── pip3
│   ├── pip3.11
│   ├── python -> /home/adsr/miniconda3/bin/python
│   ├── python3 -> python
│   └── python3.11 -> python
├── include
│   └── python3.11
├── lib
│   └── python3.11
│       └── site-packages

Make a new environment for every project! Or don’t, and find out what happens:

There are several package managers for python, poetry and conda are the most popular5 as of 2024. You can use pip, it’s fine.

5 My own assertion, no data to back this up, probably true.

Modules

When you make a .py file with any definitions, it’s called a module, and the module name is the file name (before the .py extension).

Consider this module:

utils.py
from copy import deepcopy

def stringify(L: list[int]) -> list:
    L = deepcopy(L)
    L.sort()
    return str(L)

We can import it from any script in the same folder:

import utils
A = utils.stringify([3, 2, 2, 1])
print(A, type(A))
[1, 2, 2, 3] <class 'str'>

We can also import component definitions:

from utils import stringify
A = stringify(["otis", "carl", "bruce"])
print(A, type(A))
['bruce', 'carl', 'otis'] <class 'str'>

Python doesn’t enforce type hints 🙃

Classes

Python has excellent support for objects (classes), even though they aren’t necessary for basic scripts.

class Node:
    def __init__(self, state, parent=None):
        self.state = state
        self.parent = parent

    def __str__(self): # this determines the string representation of the node
        return str(self.state)

The __init__ function is the constructor. We’ll try to unpack what happens below:

a = Node([2, 3])
print(a, a.parent)

b = Node([2, 4], a)
a = 3
print(b.parent)
[2, 3] None
[2, 3]
  • The constructor is defined as __init__ but is called with the class name
  • We overrode __str__ so that printing a Node prints its state variable
  • We reassigned a to an integer, 3
  • b still has a valid .parent reference!
c = b.parent
c.state = [2, 5]
print(b.parent)
[2, 5]

References?

Surely you are aware that Python doesn’t have pointers.

…Python doesn’t have pointers in the sense that it does not have pointers that directly reference locations in memory. Python does have references, which point to objects in namespaces, and they are simultaneously extremely useful and extremely confusing. 🙃

  • Primitive/immutable types are assigned ‘directly’
  • Objects are assigned as references

To illustrate:

Ints are immutable (so are floats and strings) \(\rightarrow\) assignment is to the value

x = 2
y = x
x = 3
print(y)
2

Lists are objects \(\rightarrow\) assignment is a reference

A = [1, 2, 3]
B = A
A.append(4)
print(B)
[1, 2, 3, 4]

Tuples are immutable \(\rightarrow\) assignment is to the value

A = 1, 2, 3
B = A
A = 4, 5, 6
print(B)
(1, 2, 3)

Dicts are objects \(\rightarrow\) assignment is a reference

A = {"carl": 5, "otis": 6}
B = A
B["bruce"] = 4
print(A)
{'carl': 5, 'otis': 6, 'bruce': 4}

Strings are immutable \(\rightarrow\) assignment is to the value

  • If you want to just access the values of a list or dict, but not the object as a reference, use copy.deepcopy
A = "otis"
B = A
A = "carl"
print(B)
otis

Hashing

Anything that’s immutable can be hashed (can be the key of a dict):

D = {}
D["first"] = 3
D[(2, 3)] = 4
D[1] = 1
print(D)
{'first': 3, (2, 3): 4, 1: 1}

Functions as References

Functions are objects, too.

Recall:

utils.py
from copy import deepcopy

def stringify(L: list[int]) -> list:
    L = deepcopy(L)
    L.sort()
    return str(L)
from utils import stringify
print(stringify)
x = stringify # what.
x([1, 4, 5])
<function stringify at 0x10459a660>
'[1, 4, 5]'

We can pass them as arguments:

def f(x, y):
    return x(y) + "!!"

a = f(stringify, ["sun", "set"])
print(a)
['set', 'sun']!!

Comprehensions

They start out kind of cute

x = [i**2 % 24 for i in range(2, 15)]
print(x)
[4, 9, 16, 1, 12, 1, 16, 9, 4, 1, 0, 1, 4]
x = [i**2 % 24 for i in range(2, 15) if i % 3 == 1]
print(x)
[16, 1, 4, 1]

They rapidly become kind of cursed and unreadable:

y = [j+i if i % 2 == 1 else "otis" for j, i in enumerate(x)]
print(y)
['otis', 2, 'otis', 4]

Dict comprehensions exist:

z = {i:j for i, j in enumerate(x)}
print(z)
{0: 16, 1: 1, 2: 4, 3: 1}

Don’t 🏌️

[print(i+j, end=" ") for i, j in enumerate([x+int(x**1.5) for x in range(2, 19)])]
print(":)")
4 9 14 19 24 30 36 43 49 56 63 70 78 86 94 102 110 :)