1. Python Syntax Whitespace and indentation Python uses whitespace/indentation to structure code instead of {}. A compound statement contains one or

Python Study Guide — Part 2

Types, Casting, datetime, Control Flow, Loops, and Boolean Logic

This guide expands your notes into a detailed, test-focused study guide. I’ll explain not just what each concept does, but why the examples produce their results, since your notes suggest the test may ask you to predict output.


1. Equality vs. Object Identity

This is one of the most important Python concepts in your notes.

There are two different questions you can ask:

  1. Do these objects have the same value?

  2. Are these names pointing to the exact same object?

These are not the same question.


== — Equality

The == operator checks whether two objects have equal values.

a = [1, 2]
b = a
c = list(a)

print(a == b)
print(a == c)

Output:

True
True

Why?

All three lists contain:

[1, 2]

So they have equal values.


2. is — Identity

The is operator asks:

Are these two names referring to the same object?

Consider:

a = [1, 2]
b = a
c = list(a)

b = a creates another binding to the same list.

a ─────┐
       ↓
     [1, 2]
       ↑
b ─────┘

But:

c = list(a)

creates a new list object.

Conceptually:

a ─────→ [1, 2]

b ─────→ [1, 2]

c ─────→ [1, 2]

The contents are the same, but c is a different object.

Therefore:

print(a is b)
print(a is c)

gives:

True
False

3. == vs. is

Memorize this:

Operator

Question

==

Do they have equal values?

!=

Do they have unequal values?

is

Are they the same object?

is not

Are they different objects?

Example

a = [1, 2]
b = [1, 2]

Then:

a == b

is:

True

because the values are equal.

But:

a is b

is:

False

because they are two separate list objects.


4. id()

id() gives an object's identity.

For example:

a = [1, 2]
b = a
c = list(a)

print(id(a))
print(id(b))
print(id(c))

You should expect:

id(a) == id(b)
id(a) != id(c)

because a and b refer to the same object.

Important equivalence

a is b

is conceptually equivalent to:

id(a) == id(b)

Similarly:

a is not b

corresponds to:

id(a) != id(b)

Test question

If:

a = [1, 2]
b = a
c = list(a)

which are true?

a == b       # True
a == c       # True
a is b       # True
a is c       # False
id(a) == id(b)  # True
id(a) == id(c)  # False

5. None

None is Python's standard null/absence-of-value object.

None

Its type is:

type(None)

which gives:

NoneType

Important

None is not the same as:

0

or:

False

or:

""

They are different objects/values.


6. None as a Function Return Value

If a function doesn't explicitly return something, it returns None.

Example:

def hello():
    print("Hello")

Calling:

x = hello()

prints:

Hello

but:

x

is:

None

because the function has no return statement.

Example

def add_one(x):
    x + 1

This does not return x + 1.

You need:

def add_one(x):
    return x + 1

Without return, the result is None.


7. None as a Default Argument

None is frequently used to indicate that an optional argument wasn't provided.

For example:

def greet(name=None):
    if name is None:
        print("Hello!")
    else:
        print("Hello", name)

Then:

greet()

uses the default:

None

8. Type Casting

Type casting means converting an object/value to another type.

Common casting functions include:

str()
int()
float()
bool()
list()

9. str()

str() converts an object into a string representation.

str(5)

gives:

"5"

You can also convert more complicated objects:

str([1, 3])

gives something like:

"[1, 3]"

Notice the difference:

[1, 3]

is a list.

While:

"[1, 3]"

is a string.


10. list()

list() can convert an iterable into a list.

For example:

k = "Python"
list(k)

produces:

['P', 'y', 't', 'h', 'o', 'n']

Why?

Because a string is a sequence of Unicode characters.

So:

"Python"

can be thought of as:

P y t h o n

and list() puts those characters into a list.


11. Strings Are Both Scalar and Sequence Types

Your notes make an important observation:

Strings are a scalar type and a sequence type.

For example:

word = "Python"

You can access individual characters:

word[0]

→ "P"

You can slice:

word[1:4]

→ "yth"

And convert to a list:

list(word)

→ ['P', 'y', 't', 'h', 'o', 'n']


12. Common Casting Functions

Know these:

Function

Converts to

str(x)

string

int(x)

integer

float(x)

float

bool(x)

Boolean

list(x)

list

Examples:

int("5")

→ 5

float("5")

→ 5.0

str(5)

→ "5"

bool(1)

→ True

bool(0)

→ False


13. datetime

Python has a built-in datetime module for working with dates and times.

You can import useful classes with:

from datetime import datetime, date, time

14. Creating a datetime

Example:

dt = datetime(2025, 8, 26, 18, 30, 0)

The arguments represent:

year
month
day
hour
minute
second

So this represents:

August 26, 2025 at 6:30:00 PM

15. Getting the Date

Given:

dt = datetime(2025, 8, 26, 18, 30, 0)

you can use:

dt.date()

to get just the date portion.

Conceptually:

2025-08-26

16. Current Date and Time

datetime.now()

returns the current date and time.

For example, it could look like:

2026-09-23 22:38:...

The exact result depends on when the code is run.


17. Formatting Dates with strftime()

strftime() converts a datetime into a formatted string.

Example:

dt.strftime("%m-%d-%Y %H:%M")

Possible result:

08-26-2025 18:30

Important formatting codes

Code

Meaning

%Y

4-digit year

%y

2-digit year

%m

month as number

%d

day

%H

hour, 24-hour clock

%M

minute

%S

second

%B

full month name

%b

abbreviated month name

Example:

dt.strftime("%B")

returns:

August

18. strptime() — String → Datetime

This is extremely important.

strptime() parses a string into a datetime object.

Example:

newdt = datetime.strptime("20090131", "%Y%m%d")

The format:

%Y%m%d

means:

4-digit year + 2-digit month + 2-digit day

So:

20090131

means:

January 31, 2009

Memorize:

strptime = string → datetime
strftime = datetime → string

A useful mnemonic:

P = Parse → strptime

F = Format → strftime


19. timedelta

When you subtract two datetime objects:

datetime.now() - newdt

Python produces a timedelta object.

Example:

duration = datetime.now() - newdt

Then:

type(duration)

will be:

datetime.timedelta

A timedelta represents a duration of time.

For example:

5000 days, 3:42:10

This is particularly useful when working with time-series data.


20. strftime vs. strptime

This is a likely test question.

strftime

Datetime → String

dt.strftime("%Y-%m-%d")

strptime

String → Datetime

datetime.strptime("2025-08-26", "%Y-%m-%d")

Remember

strftime = format
strptime = parse

21. Control Flow

Control flow determines which statements execute and when.

The major control-flow statements in your notes are:

  • if

  • for

  • while


22. if Statements

Basic structure:

if condition:
    statement

Example:

if x > 5:
    print("Large")

The part:

if x > 5:

is the header.

The indented code:

print("Large")

is the body/suite.


23. if / else

if condition:
    # runs if condition is True
else:
    # runs if condition is False

Example:

if x > 5:
    print("Large")
else:
    print("Small")

This is a multi-clause statement.


24. if / elif / else

You can have multiple conditions:

if x > 10:
    print("large")
elif x > 5:
    print("medium")
else:
    print("small")

Python evaluates the conditions from top to bottom and executes the appropriate branch.


25. for Loops

A for loop repeats code for each item in an iterable.

Example:

for char in "Python":
    print(char)

Output:

P
y
t
h
o
n

26. Combining for and if

Your notes use:

vowels = {"a", "e", "i", "o", "u"}

for i in "Python":
    if i in vowels:
        print(f"vowel: {i}")
    else:
        print(f"consonant: {i}")

Let's break it down.

Step 1

for i in "Python":

Each character is assigned to i.

Step 2

if i in vowels:

Checks whether the character is a member of the vowels set.

Step 3

If true:

print(f"vowel: {i}")

Otherwise:

print(f"consonant: {i}")

27. f-Strings

This syntax:

f"vowel: {i}"

is an f-string.

It allows you to insert variables into strings.

Example:

name = "Sam"
age = 20

print(f"My name is {name} and I am {age}.")

Output:

My name is Sam and I am 20.

Remember:

f"...{variable}..."

28. Conditions Can Be Compound

A condition doesn't have to be simple.

Example:

if x > 5 and y < 10:
    print("Yes")

Multiple logical conditions can be combined with:

and
or
not

29. Short-Circuit Evaluation

Python evaluates Boolean expressions from left to right and can stop early.

and

For:

A and B

if A is false, Python doesn't need to evaluate B.

Example:

False and something

The result is already determined.

or

For:

A or B

if A is true, Python doesn't need to evaluate B.

Example:

True or something

The result is already determined.

This is called short-circuit evaluation.


30. break

break immediately exits the loop.

Example:

for i in range(10):
    if i == 5:
        break
    print(i)

Output:

0
1
2
3
4

When i becomes 5, break exits the loop.

Remember

break = leave the loop


31. continue

continue does something different.

It skips the rest of the current iteration and moves to the next iteration.

Example:

for i in range(5):
    if i == 2:
        continue
    print(i)

Output:

0
1
3
4

The loop doesn't end.

It simply skips 2.

Remember

continue = skip this iteration

break = stop the loop


32. break vs. continue

Keyword

What happens?

break

Completely exits the loop

continue

Skips to the next iteration

pass

Does nothing

This distinction is very testable.


33. pass

pass means:

Do nothing.

Example:

if x > 5:
    pass

This is syntactically valid even though nothing happens.

Why is pass useful?

Python uses indentation to determine where clauses end, so sometimes you need a statement in a block even when you don't want that block to do anything yet.

Example:

for x in values:
    if x < 0:
        pass

34. range()

range() produces a sequence of integers.

The common forms are:

range(stop)
range(start, stop)
range(start, stop, step)

range(stop)

range(5)

represents:

0, 1, 2, 3, 4

Notice that 5 is excluded.


range(start, stop)

range(2, 6)

represents:

2, 3, 4, 5

range(start, stop, step)

range(1, 10, 2)

represents:

1, 3, 5, 7, 9

The stop value is still excluded.


35. Example: Every Fourth Letter

Your notes use:

word = "abcdefghijklmnopqrstuvwxyz"

[word[i] for i in range(1, len(word), 4)]

Let's break it down.

len(word)

is:

26

So:

range(1, 26, 4)

produces:

1, 5, 9, 13, 17, 21, 25

Those are the indices selected from the string.

Because Python starts indexing at 0:

a = 0
b = 1
c = 2
...
z = 25

Therefore the result is:

['b', 'f', 'j', 'n', 'r', 'v', 'z']

36. List Comprehensions

This:

[word[i] for i in range(1, len(word), 4)]

is a list comprehension.

General structure:

[expression for variable in iterable]

Example:

[x * 2 for x in range(5)]

produces:

[0, 2, 4, 6, 8]

You can think of it as a compact way to build a list using a loop.


37. Boolean Expressions — Very Important

Your Boolean quiz contains some concepts that are extremely important because Python's and and or do something slightly different from what beginners often expect.

Python's and and or don't necessarily return True or False.

They can return one of their operands.


38. or

Consider:

True or 2

Result:

True

Because the first operand is truthy, Python stops.

But:

2 or True

returns:

2

Why?

2 is truthy, so Python doesn't need to evaluate the second operand.

Key rule

For:

A or B

if A is truthy, the result is A.

Otherwise, the result is B.


39. and

For:

A and B

if A is falsy, the result is A.

Otherwise, the result is B.

Example:

3 and 1

returns:

1

because 3 is truthy, so Python evaluates and returns the second operand.


40. Why bool() Matters

Remember:

bool(2)

is:

True

But:

2 == True

is:

False

These are different operations.

Why?

bool(2) asks:

What is the truth value of 2?

Since any nonzero integer is truthy:

True

But:

2 == True

asks:

Is the value 2 equal to the value True?

In Python:

True == 1

is True, but:

2 == True

is False.


41. True and False Behave Like 1 and 0

In numerical comparisons:

True == 1

→ True

and:

False == 0

→ True

But don't confuse this with truthiness.

For example:

bool(2)

is True, but:

2 == True

is False.


42. Understanding the Boolean Quiz

Let's work through each one.

Expression

Result

Why

True or 2

True

First operand is truthy

2 or True

2

2 is truthy

bool(2)

True

Nonzero numbers are truthy

2 == True

False

2 != 1

1 == True

True

True equals 1

2 < 1 or 3

3

First part is false, so return second operand

3 and 1 < 2

True

3 truthy, then 1 < 2 is True

1 < 2 and 3

3

First part true, so return 3

1 < 2 & 3

True

Operator precedence makes this a bitwise expression

2 or not 3

2

2 is truthy, so or stops


43. The Tricky One: 2 < 1 or 3

Start with:

2 < 1

which is:

False

So:

False or 3

returns:

3

It does not return True.

This illustrates that or returns an operand.


44. The Tricky One: 3 and 1 < 2

Operator precedence means the comparison happens:

1 < 2

→ True

So:

3 and True

Since 3 is truthy:

True

45. The Tricky One: 1 < 2 and 3

First:

1 < 2

→ True

Then:

True and 3

returns:

3

Important

and doesn't necessarily return a Boolean.


46. The Tricky One: 2 or not 3

First:

not 3

would be:

False

But Python doesn't even need to evaluate it because:

2

is already truthy.

So:

2 or not 3

returns:

2

This is short-circuit evaluation.


47. The Very Tricky One: 1 < 2 & 3

This is different from:

1 < 2 and 3

because & is a bitwise operator, not Boolean and.

Operator precedence means the expression is evaluated in terms of the bitwise operation before the comparison.

Conceptually:

2 & 3

is:

2

because:

2 = 10
3 = 11
    --
    10

Then:

1 < 2

is:

True

So the result is:

True

Test warning

Don't replace:

and

with:

&

They are not interchangeable.


48. Operator Precedence

Operator precedence determines which operations happen first.

A simplified hierarchy useful for this material is:

parentheses
    ↓
arithmetic
    ↓
bitwise operations
    ↓
comparisons
    ↓
not
    ↓
and
    ↓
or

The exact full Python precedence table is more detailed, but the key point from your notes is:

Comparisons happen before Boolean and/or, while bitwise operators have different precedence.

When uncertain, use parentheses.

For example:

(1 < 2) and 3

is much easier to understand than relying on precedence.


49. while Loops

Your notes mention while as another major control-flow structure.

A while loop continues while its condition is truthy.

x = 0

while x < 5:
    print(x)
    x += 1

Output:

0
1
2
3
4

Structure

while condition:
    body

Like if and for, it uses:

  • Header

  • Colon

  • Indented suite/body


50. Control Flow Summary

Statement

Purpose

if

Execute code conditionally

elif

Check another condition

else

Execute when previous conditions are false

for

Iterate over a sequence/iterable

while

Repeat while condition is truthy

break

Exit loop

continue

Skip current iteration

pass

Do nothing


51. Key Vocabulary to Know

Your professor uses specific terminology. Be able to recognize these terms.

Object

A value/entity in Python with a type, attributes, and methods.

Attribute

Data associated with an object.

Accessed with:

object.attribute

Method

A function associated with an object.

Example:

list.sort()

Namespace

A mapping between names and objects.

Binding

The relationship between a name and an object.

Mutable

Can be changed in place.

Immutable

Cannot be changed in place.

Scope

Where a name is visible.

Casting

Converting an object/value to another type.

Control flow

Determines which statements execute and in what order.

Short-circuit evaluation

Python stops evaluating a Boolean expression once its result is already determined.


⭐ Highest-Priority Test Concepts

If you're studying the night before the exam, focus heavily on these.

Tier 1 — Absolutely know

  1. == vs. is

  2. id()

  3. Mutable vs. immutable

  4. Assignment/binding

  5. None

  6. type()

  7. isinstance()

  8. str(), int(), float(), bool(), list()

  9. strftime() vs. strptime()

  10. if, for, while

  11. break vs. continue vs. pass

  12. range(start, stop, step)

  13. Boolean short-circuiting

  14. and vs. or

  15. and/or can return operands rather than True/False

  16. and vs. &


🧠 Must-Know Examples

Be able to predict these without running Python.

Example 1

a = [1, 2]
b = a
c = list(a)

print(a == b)
print(a == c)
print(a is b)
print(a is c)

Answer:

True
True
True
False

Example 2

a = [1, 2]
b = a

a.append(3)

print(b)

Answer:

[1, 2, 3]

Because a and b refer to the same mutable list.


Example 3

a = 5
b = a

a = 6

print(b)

Answer:

5

Because a = 6 binds a to a different integer object. It doesn't modify the original integer.


Example 4

def f():
    print("Hello")

x = f()

print(x)

Answer:

Hello
None

Example 5

print(2 or True)

Answer:

2

Example 6

print(1 < 2 and 3)

Answer:

3

Example 7

print(2 == True)
print(1 == True)

Answer:

False
True

Example 8

for i in range(2, 8, 2):
    print(i)

Answer:

2
4
6

Example 9

for i in range(5):
    if i == 2:
        continue
    print(i)

Answer:

0
1
3
4

Example 10

for i in range(5):
    if i == 2:
        break
    print(i)

Answer:

0
1

🔥 Final Memorization Sheet

==          equal values
!=          unequal values
is          same object
is not      different objects

id(x)       identity of x

None        Python's null/absence-of-value object
NoneType    type(None)

str()       → string
int()       → integer
float()     → float
bool()      → Boolean
list()      → list

strftime    datetime → string
strptime    string → datetime

timedelta   duration between datetime objects

if          conditional execution
for         iteration
while       repeated execution while condition is true

break       leave loop
continue    skip current iteration
pass        do nothing

range(5)          0,1,2,3,4
range(2,5)        2,3,4
range(1,10,2)     1,3,5,7,9

and         Boolean AND; may return an operand
or          Boolean OR; may return an operand
not         logical negation

&           bitwise AND
|           bitwise OR
^           bitwise XOR
~           bitwise complement

True == 1
False == 0

bool(2)     True
2 == True   False

"string"    immutable
list        mutable

The Big Picture

The most important conceptual chain in this section is:

Names → objects → types → identity/value → mutability → operations → control flow.

If you understand that Python variables are names bound to objects, then == vs. is, id(), mutable vs. immutable objects, function arguments, and many of the Boolean/control-flow examples become much easier to reason about.

Python Native Types — Detailed Study Guide

1. Big Picture: Python Collection Types

Before getting into each type, memorize this comparison:

Type

Syntax

Ordered/Sequence?

Mutable?

Duplicates?

Access by index?

Tuple

(1, 2, 3)

Yes

❌ No

Yes

Yes

List

[1, 2, 3]

Yes

✅ Yes

Yes

Yes

Dictionary

{"a": 1}

Mapping

✅ Yes

Keys unique

By key

Set

{1, 2, 3}

Unordered

✅ Yes

❌ No

❌ No

Quick memory trick

  • Tuple = list that can't be changed

  • List = changeable sequence

  • Dictionary = key → value

  • Set = unique collection


2. Tuples

A tuple is:

A fixed-length, immutable sequence that can contain objects of different types.

Example:

a = ([1, 2], "A+", {"grade": "F-"})

Notice that the tuple contains three different objects:

[1, 2]
"A+"
{"grade": "F-"}

The objects don't have to be the same type.


3. Creating Tuples

Tuples can be written with parentheses:

b = ([1, 2], "A+", {"grade": "F-"})

But parentheses aren't actually required.

a = [1, 2], "A+", {"grade": "F-"}

Both create tuples.

Important

The commas are what make the tuple.

For example:

x = (5)

is just an integer:

type(x)
# int

But:

x = (5,)

is a tuple:

type(x)
# tuple

Test question

What is the difference?

(5)

vs.

(5,)

Answer:

  • (5) → int

  • (5,) → tuple


4. Tuple Immutability

Tuples cannot be modified directly.

For example:

a = (1, 2, 3)

You cannot do:

a[0] = 10

That produces an error because tuples are immutable.


5. But Tuples Can Contain Mutable Objects

This is a very important subtlety.

Consider:

a = ([1, 2], "A+", {"grade": "F-"})

The tuple itself cannot be changed.

But the dictionary inside the tuple is mutable.

Therefore:

a[2]["grade"] = "C+"

is allowed.

The tuple still contains the same dictionary object, but the dictionary itself has changed.

Before:

(
    [1, 2],
    "A+",
    {"grade": "F-"}
)

After:

(
    [1, 2],
    "A+",
    {"grade": "C+"}
)

Key distinction

The tuple is immutable, but objects contained inside the tuple might be mutable.


6. Tuple Methods and Operations

Tuples support many sequence operations.

Concatenation

a + b

combines two tuples into a new tuple.

Example:

a = (1, 2)
b = (3, 4)

a + b

Result:

(1, 2, 3, 4)

Repetition

You can use *:

b * 2

If:

b = (3, 4)

then:

b * 2

produces:

(3, 4, 3, 4)

7. Tuple Unpacking

You can assign the individual elements of a tuple to variables.

a = (10, 20, 30)

x, y, z = a

Now:

x = 10
y = 20
z = 30

This is called tuple unpacking.

Important

The number of variables normally has to match the number of values.

x, y = (1, 2)

works.

But:

x, y = (1, 2, 3)

causes an error because there are too many values.


8. Unpacking in a for Loop

You can unpack tuples while iterating.

Example:

seq = [(1, 2, 3), (4, 5, 6)]

for c, d, e in seq:
    print(f"c = {c}, d = {d}, e = {e}")

First iteration:

c = 1
d = 2
e = 3

Second:

c = 4
d = 5
e = 6

Python automatically unpacks each tuple.


9. *args and **kwargs

You may see these in function definitions and documentation.

The important idea:

*

Used for variable-length positional arguments.

**

Used for variable-length keyword arguments.

For example:

def f(*args):
    print(args)

Calling:

f(1, 2, 3)

makes args a tuple:

(1, 2, 3)

10. Extended Tuple Unpacking

You can use * when unpacking.

Example:

a = (1, 2, 3)

p, *args = a

Result:

p = 1
args = [2, 3]

Important detail

The *args variable receives the remaining values as a list in an unpacking assignment.

So:

p, *args = (1, 2, 3)

gives:

p == 1
args == [2, 3]

11. Lists

A list is:

A variable-length, mutable sequence that can contain heterogeneous objects.

Example:

a = [[1, 2], "A+", {"grade": "F-"}]

Lists use square brackets:

[1, 2, 3]

12. Lists Are Mutable

Unlike tuples, lists can be changed.

a = [1, 2, 3]

a[0] = 100

Now:

a

is:

[100, 2, 3]

13. list()

You can create a list using list().

For example:

b = list((3, 4, "VSCode", {"grade": "C+"}))

This converts the tuple into a list.

Result:

[3, 4, "VSCode", {"grade": "C+"}]

14. list() and Iterators

list() is also useful for materializing an iterator.

For example:

gen = range(10)

gen represents a range of numbers.

list(gen)

produces:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Vocabulary

Materialize means essentially:

Turn the iterable/iterator's values into an actual collection that you can see and work with.


15. .insert()

The insert() method adds an item at a specific index.

Syntax:

list.insert(index, value)

Example:

a = [1, 2, 3]

a.insert(1, "hello")

Result:

[1, "hello", 2, 3]

Important

The existing elements are shifted over.


16. .pop()

pop() removes an item by index and returns the removed item.

Example:

b = [1, 2, 3, 4]

x = b.pop(2)

Now:

x

is:

3

and:

b

is:

[1, 2, 4]

Remember

pop → position/index

17. .append()

append() adds one object to the end of a list.

a = [1, 2]

a.append(3)

Result:

[1, 2, 3]

Important

If you append a list:

a.append([4, 5])

you get:

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

The entire list [4, 5] is added as one item.


18. .extend()

extend() adds multiple items to the end of a list.

a = [1, 2]

a.extend([3, 4])

Result:

[1, 2, 3, 4]

append() vs. extend()

This is highly testable.

a = [1, 2]
a.append([3, 4])

→

[1, 2, [3, 4]]

But:

a = [1, 2]
a.extend([3, 4])

→

[1, 2, 3, 4]

Memory trick

append = add one thing

extend = add the contents of another iterable


19. .remove()

remove() removes an item based on its value, not its index.

a = [1, 2, 3, 2]

a.remove(2)

Result:

[1, 3, 2]

Only the first matching value is removed.

Compare:

pop(2)

means:

Remove the item at index 2.

While:

remove(2)

means:

Find the value 2 and remove its first occurrence.


20. List Concatenation: + vs .extend()

These accomplish similar-looking tasks differently.

+

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

c = a + b

Creates a new list.

a → [1, 2]
b → [3, 4]
c → [1, 2, 3, 4]

a remains unchanged.

.extend()

a.extend(b)

modifies a itself.

Now:

a

is:

[1, 2, 3, 4]

Important

+ creates a new list.

.extend() modifies the existing list.


21. Membership in Lists

You can check whether an item exists:

3 in [1, 2, 3]

→ True

Or:

4 not in [1, 2, 3]

→ True

Operators:

in
not in

22. Lists vs. Sets for Searching

Your notes mention efficiency.

Lists generally require searching through elements one by one.

Sets use hash tables, which generally provide much faster membership testing.

Therefore, if you're frequently asking:

x in collection

a set can be much more efficient when the data can appropriately be represented as a set.

Important conceptual distinction

Use a:

  • list when sequence/order and duplicates matter

  • set when uniqueness and fast membership testing matter


23. .sort()

A list can be sorted in place.

Example:

a = ["stat", "I", "love", "mathematics"]

a.sort()

The list itself changes.

Important

.sort() modifies the original list.

It does not create a separate sorted list.


24. .sort(key=len)

You can specify how Python should sort the elements.

a.sort(key=len)

This means:

Sort the strings according to their length.

For:

["stat", "I", "love", "mathematics"]

the lengths are:

stat         → 4
I            → 1
love         → 4
mathematics  → 11

So the result will be ordered by length.


25. Slicing

One of the most important list/sequence concepts:

[start:stop:step]

Key rule

Start is included; stop is excluded.

Example:

a = ["a", "b", "c", "d", "e"]
a[1:4]

gives:

["b", "c", "d"]

Index 4 is not included.


26. Basic Slicing

a[2:3]

Start at index 2, stop before 3.

a[2:3]

returns one element.


a[:2]

Means:

Start at the beginning, stop before index 2.

a[:2]

a[2:]

Means:

Start at index 2 and go to the end.


a[:-1]

Means:

Start at the beginning and stop before the last element.


a[-1:]

Means:

Start at the last element and go through the end.

So this produces a one-element list containing the last item.


a[::2]

Means:

Take every second element.


27. Negative Indexing

Python allows negative indexes.

For:

a = ["a", "b", "c", "d"]

the indexes are:

   a    b    c    d
   ↑    ↑    ↑    ↑
   0    1    2    3
  -4   -3   -2   -1

Therefore:

a[-1]

is:

"d"

and:

a[-2]

is:

"c"

28. Dictionary

A dictionary is a mutable collection of key-value pairs.

Syntax:

{
    key: value,
    key: value
}

Example:

a = {
    "name": "Sam",
    "grade": "A"
}

Conceptually:

"name"  → "Sam"
"grade" → "A"

29. Empty Dictionary

This:

a = {}

creates an empty dictionary.

Important!

It does not create an empty set.

An empty set is:

set()

This is a common test question.

{}     → empty dictionary
set()  → empty set

30. Creating Dictionaries with dict()

You can construct a dictionary from pairs:

dict(
    (("first", 1),
     ("second", 2),
     ("third", 3))
)

Result:

{
    "first": 1,
    "second": 2,
    "third": 3
}

31. Dictionary Access

Use the key to access the value.

a["third"]

If:

a = {
    "first": 1,
    "second": 2,
    "third": 3
}

then:

a["third"]

returns:

3

32. Adding or Modifying Dictionary Values

You can assign directly:

a["fourth"] = 4

If "fourth" doesn't exist, it is added.

If it already exists, its value is changed.

Example:

a["first"] = 100

changes the existing value.


33. .update()

You can add or modify several key-value pairs at once.

a.update({
    "first": 1,
    "second": 2,
    "third": 3
})

34. .pop() for Dictionaries

Dictionary .pop() removes a key and returns its value.

a.pop("third")

If:

a["third"] = 3

then:

a.pop("third")

returns:

3

and removes "third" from the dictionary.

Compare list and dictionary pop()

List:

a.pop(2)

→ removes by index

Dictionary:

a.pop("third")

→ removes by key


35. del with Dictionaries

You can delete a key-value pair:

del a["fourth"]

The "fourth" key and its value are removed.


36. list(dictionary)

When you convert a dictionary to a list:

list(a)

Python returns a list of the keys.

Example:

a = {
    "first": 1,
    "second": 2
}

Then:

list(a)

produces:

["first", "second"]

Important

It does not return the values.


37. sorted(dictionary)

When you use:

sorted(a)

Python sorts the dictionary's keys.

The result is a list.

Example:

a = {
    "z": 1,
    "a": 2,
    "m": 3
}

Then:

sorted(a)

produces:

["a", "m", "z"]

38. .keys()

Returns an iterable/view of the dictionary's keys.

a.keys()

You can iterate over it:

for key in a.keys():
    print(key)

39. .values()

Returns the dictionary's values.

a.values()

Example:

a = {
    "a": 10,
    "b": 20
}

The values are:

10
20

40. .items()

Returns key-value pairs.

a.items()

You can unpack them:

for key, value in a.items():
    print(key, value)

This is extremely common Python syntax.


41. Dictionary Summary

Memorize:

dict.keys()    → keys
dict.values()  → values
dict.items()   → (key, value) pairs

And:

list(dict)     → keys
sorted(dict)   → sorted keys

42. Dictionary Keys Must Be Hashable

Dictionary values can be almost any object.

But keys must be hashable.

Examples of hashable objects:

"Python"
5
3.14
(1, 2)

Lists are not hashable:

[1, 2]

so you cannot use a list as a dictionary key.


43. What Does "Hashable" Mean?

A hashable object can produce a stable hash value.

You can test an object with:

hash("Python")

If Python can hash it, it can generally be used as a dictionary key or set element.

Common rule

Immutable objects are often hashable.

Mutable objects such as lists and dictionaries are not hashable.


44. zip()

zip() combines multiple iterables element-by-element.

Example:

names = ["A", "B", "C"]
numbers = [1, 2, 3]

zip(names, numbers)

Conceptually produces:

("A", 1)
("B", 2)
("C", 3)

To see the results:

list(zip(names, numbers))

gives:

[("A", 1), ("B", 2), ("C", 3)]

45. zip() and Dictionaries

Your notes use:

v = list("Python")
k = range(len(v))

dict(zip(k, v))

Let's break it down.

v

is:

["P", "y", "t", "h", "o", "n"]

and:

k

represents:

0, 1, 2, 3, 4, 5

zip(k, v) pairs them:

0 → P
1 → y
2 → t
3 → h
4 → o
5 → n

Then dict() creates:

{
    0: "P",
    1: "y",
    2: "t",
    3: "h",
    4: "o",
    5: "n"
}

46. zip() Stops at the Shortest Iterable

This is very important.

Suppose:

a = [1, 2, 3, 4]
b = ["a", "b"]

Then:

list(zip(a, b))

produces:

[(1, "a"), (2, "b")]

The extra 3 and 4 are ignored.

Rule

zip() stops when the shortest input runs out of values.


47. enumerate()

enumerate() is extremely useful when you need both:

  • the index

  • the value

Instead of:

for i in range(len(my_data)):
    print(i, my_data[i])

you can write:

for index, value in enumerate(my_data):
    print(index, value)

Example:

my_data = [1, 2, 3, 4]

produces pairs conceptually:

(0, 1)
(1, 2)
(2, 3)
(3, 4)

48. enumerate() vs. zip()

enumerate()

Adds an index:

enumerate(["a", "b", "c"])

→

(0, "a")
(1, "b")
(2, "c")

zip()

Combines multiple sequences:

zip([1, 2], ["a", "b"])

→

(1, "a")
(2, "b")

49. reversed()

reversed() allows you to iterate through an iterable in reverse order.

Example:

x = [1, 2, 3]

list(reversed(x))

returns:

[3, 2, 1]

50. Iterators

An iterator produces values one at a time.

Functions such as:

zip()
enumerate()
reversed()

can produce iterator-like objects.

For example:

rev_obj = reversed([1, 2, 3])

You may not immediately see the actual values when printing rev_obj.

You can materialize them:

list(rev_obj)

→

[3, 2, 1]

51. Important: Iterators Can Be Consumed

If an object is an iterator, its values are generally produced as you iterate through it.

For example:

z = zip([1, 2], ["a", "b"])

list(z)

produces:

[(1, "a"), (2, "b")]

If you then do:

list(z)

again, you may get:

[]

because the iterator has already been consumed.

Key idea

An iterator produces values as you consume it.


52. Sets

A set is an unordered collection of unique elements.

Example:

{1, 2, 3}

53. Sets Automatically Remove Duplicates

Consider:

set([2, 2, 2, 1, 3, 3])

The duplicates disappear.

Result conceptually:

{1, 2, 3}

The exact display order should not be relied upon.


54. Sets Are Unordered

You cannot use an index:

my_set[0]

This is invalid.

A set doesn't have the sequence-style indexing of lists and tuples.

Remember

list    → indexed
tuple   → indexed
set     → not indexed
dict    → accessed by key

55. Sets Require Hashable Elements

Like dictionary keys, set elements must be hashable.

This works:

{1, 2, 3}

This doesn't:

{[1, 2, 3]}

because lists are mutable and unhashable.


56. Putting a List into a Set

If you need a list-like object as a set element, convert it to a tuple:

my_data = [1, 2, 3, 4]

my_set = {tuple(my_data)}

Now:

my_set

contains:

{(1, 2, 3, 4)}

because tuples can be hashable.


57. Set Operations

Sets support mathematical operations such as:

  • Union

  • Intersection

  • Difference

Suppose:

A = {1, 2, 3}
B = {3, 4, 5}

Union

Everything in either set:

A | B

→

{1, 2, 3, 4, 5}

Intersection

Elements in both:

A & B

→

{3}

Difference

Elements in A but not B:

A - B

→

{1, 2}

58. Set Methods vs. Operators

You can use methods:

A.union(B)

or operators:

A | B

Important difference

The method form can accept certain non-set iterables by converting them appropriately.

The binary operator form expects another set.

Your notes demonstrate:

my_set.union(my_data)

versus:

my_set | my_data

The first can work when my_data is a list; the second requires set operands.


59. In-Place Set Operations

You can combine operation + assignment.

For example:

my_set |= more_set

is essentially:

my_set = my_set | more_set

It updates the set with the union.

Similar operators include:

&=
|=
^=
-=

60. Set Equality

Set equality works based on the elements, not ordering.

For example:

{1, 2, 3} == {3, 2, 1}

is:

True

because they contain the same elements.

This is different from sequences where order matters.


61. List vs. Tuple vs. Set vs. Dictionary

This comparison is worth memorizing:

Feature

List

Tuple

Set

Dictionary

Mutable

✅

❌

✅

✅

Ordered/sequence

✅

✅

❌

Mapping

Duplicates

✅

✅

❌

Keys ❌

Indexing

✅

✅

❌

Key access

Syntax

[]

()

{}

{key: value}

Main purpose

Changeable sequence

Fixed sequence

Unique elements

Key/value mapping


62. Comprehensions

A comprehension is a concise way of creating a collection.

Instead of:

result = []

for value in collection:
    if condition:
        result.append(expr)

you can write:

[expr for value in collection if condition]

63. List Comprehension

Example:

strings = ["a", "as", "bat", "car", "dove", "python"]

[x.upper() for x in strings if len(x) > 3]

Let's break it down:

x.upper()

is what we put into the resulting list.

for x in strings

means iterate through each string.

if len(x) > 3

means only keep strings longer than 3 characters.

Result:

["BATS"?]

More precisely, the qualifying words are:

"bat"     → length 3 → excluded
"car"     → length 3 → excluded
"dove"    → length 4 → included
"python"  → length 6 → included

So the result is:

["DOVE", "PYTHON"]

64. General Comprehension Pattern

Memorize:

[expression for variable in collection if condition]

Read it almost like English:

Put expression into the list for every variable in collection if condition is true.


65. Comprehensions Without an if

You don't need a condition.

[x * 2 for x in range(5)]

Result:

[0, 2, 4, 6, 8]

66. Set Comprehensions

You can use curly braces:

{x * 2 for x in range(5)}

This creates a set.

Result:

{0, 2, 4, 6, 8}

Remember that sets automatically remove duplicates.


67. Dictionary Comprehensions

Dictionary comprehensions use:

{key: value for ...}

Example:

{x: x**2 for x in range(4)}

produces:

{
    0: 0,
    1: 1,
    2: 4,
    3: 9
}

68. Nested Comprehensions

This is one of the trickier topics.

Suppose:

two_lists = [
    ["John", "Emily", "Michael", "Mary", "Steven"],
    ["Maria", "Juan", "Javier", "Natalia", "Pilar"]
]

The comprehension:

[name.lower()
 for namelist in two_lists
 for name in namelist
 if len(name) > 5]

works like nested loops.

Equivalent expanded version:

result = []

for namelist in two_lists:
    for name in namelist:
        if len(name) > 5:
            result.append(name.lower())

69. How to Read Nested Comprehensions

Read from the outside inward, matching nested loops.

[
    name.lower()
    for namelist in two_lists
    for name in namelist
    if len(name) > 5
]

Think:

for each namelist
    for each name
        if name is longer than 5
            add lowercase name

The result is:

[
    "michael",
    "steven",
    "maria",
    "javier",
    "natalia"
]

70. Flattening a List of Lists

Suppose:

two_lists = [
    ["John", "Emily", "Michael"],
    ["Maria", "Juan", "Javier"]
]

You want:

[
    "John",
    "Emily",
    "Michael",
    "Maria",
    "Juan",
    "Javier"
]

You can use:

[name
 for namelist in two_lists
 for name in namelist]

This is called flattening the nested lists.


71. map()

map() applies a function to every element.

Your notes use:

list(map(lambda x: x[-1], strings))

The important idea is:

Apply the function to every element.

For example:

numbers = [1, 2, 3]

list(map(lambda x: x * 2, numbers))

produces:

[2, 4, 6]

72. map() vs. Comprehension

These can often accomplish similar things.

Using map():

list(map(lambda x: x * 2, numbers))

Using a comprehension:

[x * 2 for x in numbers]

Both produce:

[2, 4, 6]

Your notes emphasize comprehensions because they are often very readable for this kind of operation.


73. lambda

A lambda is a small anonymous function.

Example:

lambda x: x * 2

means roughly:

def f(x):
    return x * 2

Then:

list(map(lambda x: x * 2, [1, 2, 3]))

gives:

[2, 4, 6]

⭐ Most Important Methods to Memorize

Lists

Method

What it does

.append(x)

Add one item to end

.extend(x)

Add multiple items

.insert(i, x)

Insert at index

.pop(i)

Remove/return item at index

.remove(x)

Remove first matching value

.sort()

Sort list in place

Biggest traps

append([1, 2])

adds one list.

extend([1, 2])

adds two elements.

pop(2)

uses an index.

remove(2)

uses a value.


⭐ Dictionary Methods

Method

Meaning

.keys()

keys

.values()

values

.items()

key-value pairs

.update()

add/change multiple pairs

.pop(key)

remove key and return value


⭐ Set Operations

A | B       # union
A & B       # intersection
A - B       # difference
A ^ B       # symmetric difference

And methods:

A.union(B)
A.intersection(B)
A.difference(B)
A.symmetric_difference(B)

⭐ Functions You Need to Know

Function

Purpose

list()

Convert to list

tuple()

Convert to tuple

set()

Convert to set

dict()

Create/convert dictionary

zip()

Combine iterables

enumerate()

Produce index + value

reversed()

Iterate backward

sorted()

Return a new sorted list

hash()

Get hash value if hashable

map()

Apply function to elements


🚨 sort() vs. sorted()

This is another very important distinction.

.sort()

Only works on lists and modifies the list:

a = [3, 1, 2]
a.sort()

Now:

a

is:

[1, 2, 3]

sorted()

Returns a new sorted list:

a = [3, 1, 2]
b = sorted(a)

Now:

a = [3, 1, 2]
b = [1, 2, 3]

Memorize

.sort() → changes the list

sorted() → creates/returns a sorted list


🧠 High-Priority Test Traps

Trap 1: Empty {}

{}

is:

dictionary, not set.

Empty set:

set()

Trap 2: Tuple parentheses

(5)

is an integer.

(5,)

is a tuple.


Trap 3: Tuple immutability

A tuple can't be modified:

t[0] = 5

But a mutable object inside the tuple can be modified:

t[1].append(5)

Trap 4: append() vs. extend()

a = [1, 2]

a.append([3, 4])

→

[1, 2, [3, 4]]

while:

a = [1, 2]

a.extend([3, 4])

→

[1, 2, 3, 4]

Trap 5: pop() vs. remove()

a.pop(2)

→ remove index 2.

a.remove(2)

→ remove first value equal to 2.


Trap 6: list(dictionary)

list(my_dict)

→ keys, not values.


Trap 7: Dictionary keys

Dictionary keys must be hashable.

{[1, 2]: "hello"}

doesn't work because lists aren't hashable.

But:

{(1, 2): "hello"}

can work because tuples can be hashable.


Trap 8: Set duplicates

set([1, 1, 2, 2, 3])

becomes:

{1, 2, 3}

Trap 9: Set indexing

This doesn't work:

my_set[0]

Sets are not indexed sequences.


Trap 10: zip() length

zip([1, 2, 3], ["a", "b"])

only produces two pairs.

It stops at the shortest iterable.


Trap 11: Iterator materialization

z = zip([1, 2], ["a", "b"])

list(z)

shows the values.

But after consuming the iterator, calling list(z) again may produce an empty list

Function Details

Rules

  • A function is declared with the keyword def followed by the function signature and terminated with a colon.

    {python style=""}
    #| echo: true
    def my_func():
    print("Hello")

    my_func()

  • It's optional to use a return statement to return control back to the calling context.

    {python style=""}
    #| echo: true
    def new_func(x, y):
    return x + y

    new_func(4, 5)

  • In the absence of a return statement, a function returns None.

    {python style=""}
    #| echo: true
    def weird_func(x, y):
    x + y

    weird_func(4, 5)

  • A function can have both "positional" and "keyword" arguments.

  • Positional arguments are required, keyword arguments are optional, and all keyword arguments must follow all positional arguments.

  • When invoking a function, keyword arguments do not need to be specified with the keyword, but it is good practice to do so.

  • There's nothing prohibiting multiple return statements.

    {python style=""}
    #| echo: true
    def test_func(y, x = 5):
    x *= y
    return x
    return x + y

    test_func(4)

  • This is commonly seen in multi-clause if statements

    {python style=""}
    #| echo: true
    def test_func(y, x = 5):
    if x < y:
    x *= y
    return x
    elif x > y:
    return x

    test_func(4)


Namespace

  • A function can access any variable defined within the function ("local scope") or any higher scope, including the "global scope". For example, in the function call test_func(4), variables x and y are created in the local namespace and assigned to 5 and 4, respectively.

  • After return x is encountered, control is returned to the calling space and the local namespace is destroyed. Therefore, x and y are destroyed. The second return statement isn't executed.

  • If a variable is defined in a higher scope, it can be referenced and mutated within a function:

    {python style=""}
    #| echo: true
    a = []
    def prod_func():
    for i in range(5):
    # Modify a
    a.append(i)

    prod_func()
    a

    {python style=""}
    #| echo: true
    x = 1
    def prod_func(y):
    # Reference x
    y *= x

    prod_func(3)

  • However, you higher scope variable cannot be assigned/reassigned a value:

    {python style=""}
    #| echo: true
    try:
    x = 1
    def prod_func(y):
    # Assign to x
    x *= y

    prod_func(3)

    except Exception as e:
    # store exception object in 'e'
    print(f"An unexpected error occurred: {e}")

  • There is a way to assign/reassign a value to a variable outside of the local scope using the keywords global or nonlocal; however, this is discouraged.

Function-alikes

Lambdas

  • Lambda expressions are "anonymous functions" consisting of a single expression that is also the return value. They're defined using the keyword lambda.

    {python style=""}
    #| echo: true
    def incrementor(n):
    return lambda x: x + n

    f = incrementor(5)
    f(4)

  • Lambdas are most often used when you need to pass a short function as an argument to another function. For example,

    {python style=""}
    #| echo: true
    b = list((3, 4, "VSCode", {"grade": "C+"}))
    list(map(lambda x: type(x), b))

    {python style=""}
    #| echo: true
    pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
    pairs.sort(key = lambda x: x[1])
    pairs


Generators & Iterators

  • Python uses an "iterator protocol" to make objects iterable. For example, when you loop over keys in a dictionary:

    {python style=""}
    #| echo: true
    some_dict = {"a": 1, "b": 2, "c": 3}
    for key in some_dict:
    key.upper()

    the Python interpretor makes an iterator that accesses the elements in the dictionary container one item at a time:

    {python style=""}
    #| echo: true
    iter(some_dict)

  • Most methods that accept a list or list-like object will also accept an iterator:

    {python style=""}
    #| echo: true
    list(iter(some_dict))

  • A generator looks and feels like a function, but is specifically designed to create iterators.

  • On the surface, one difference between a function and generator is that instead of keyword return, a generator uses keyword yield.

    {python style=""}
    #| echo: true
    def reverse(data):
    for index in range(len(data)-1, -1, -1):
    yield data[index]

  • You can use next() to step through the generator. The generator remembers the data values and which statement was last executed.

    {python style=""}
    #| echo: true
    # Instantiate the reverse generator
    rev = reverse('Python')
    next(rev)
    next(rev)
    next(rev)

  • Just like list, dictionary, or set comprehensions, there is a shorthand for generator expressions:

    {python style=""}
    #| echo: true
    # Define & Instantiate the reverse generator
    rev2 = ('Python'[i] for i in range(len('Python') - 1, -1, -1))
    next(rev2)
    next(rev2)

Output of the Snippet

Evaluating the final array division (data / 20) yields:

Python

array([[ 0.075 , -0.005 ,  0.15  ],
       [ 0.    , -0.15  ,  0.325 ]])

Core Concepts Explained

1. Why NumPy Outperforms Standard Python Lists

Feature

Python Lists

NumPy ndarray

Memory Allocation

Array of pointers to generic Python objects scattered in memory.

Contiguous block of homogeneous data in memory.

Type Checking

Dynamically checks data type for every single element on every operation.

Single uniform type (dtype); C code assumes identical byte-size across elements.

Execution

Interpreted Python bytecode loop.

Compiled C code executing vector hardware instructions (SIMD).

  • Contiguous Memory: Storing elements sequentially in RAM maximizes CPU cache hits and allows modern CPUs to perform SIMD (Single Instruction, Multiple Data) operations, processing multiple numbers in a single CPU cycle.

  • No Interpreter Overhead: Operations like my_arr * 2 pass memory addresses directly to C loops, bypassing Python object creation and reference counting for each element.

2. Vectorization and Element-Wise Operations

In pure Python, transforming an array requires a loop or list comprehension:

Python

my_list2 = [x * 2 for x in my_list]

With NumPy, operations are vectorized and apply element-wise automatically across the entire array structure (including 2D matrices like data / 20):

Python

my_arr2 = my_arr * 2

This scalar operation is broadcast across all elements of the array without explicit Python loops.

3. IPython Magic Commands (%timeit)
  • %timeit is an IPython line magic command (prefixed with %).

  • It automatically runs a statement thousands of times to compute an accurate mean and standard deviation execution time.

  • On a 1×1061 \times 10^6 element array, NumPy is typically 10×10\times to 50×50\times faster than standard Python list comprehensions.