Everything you need to know for CS1, from variables to OOP. Written for university students taking their first programming course.
Every intro CS course starts here. These concepts are the building blocks for everything else.
A variable is a name that refers to a value. Python has dynamic typing - you don't declare types, but they exist and matter.
name = "Alice" # str
age = 20 # int
gpa = 3.7 # float
enrolled = True # bool
courses = ["CS", "Math"] # list
The key insight that trips up beginners: assignment creates a reference, not a copy. When you write b = a for a list, both names point to the same list. Modifying one changes both.
Type conversion questions are common. Know that int("3.5") raises an error (can't convert a string with a decimal to int directly), but int(float("3.5")) works. Know the difference between str(42) and repr(42).
Beyond basic math (+, -, *, /), know these:
// - integer (floor) division: 7 // 2 is 3% - modulo (remainder): 7 % 2 is 1** - exponent: 2 ** 10 is 1024== vs is - equality vs identity (is checks if same object in memory)The if/elif/else chain. The order matters - Python evaluates top to bottom and stops at the first match.
def letter_grade(pct):
if pct >= 90:
return "A+"
elif pct >= 85:
return "A"
elif pct >= 80:
return "A-"
elif pct >= 77:
return "B+"
else:
return "below B+"
Common mistake: using multiple if statements when you mean elif. Multiple ifs are independent checks - a student scoring 95 would match both >= 90 and >= 85.
for loops iterate over sequences:
# Iterate through a list
for course in ["CSCA08", "MATA31", "PSYA01"]:
print(course)
# Range: 0 to n-1
for i in range(5):
print(i) # 0, 1, 2, 3, 4
# Enumerate: index + value
for i, course in enumerate(courses):
print(f"{i}: {course}")
while loops repeat until a condition is false:
count = 0
while count < 10:
count += 1
Infinite loops happen when the while condition never becomes false. Always make sure the loop body changes something that affects the condition. Forgetting count += 1 in the example above would loop forever.
Functions are the most important concept in intro CS. They let you break problems into pieces, reuse code, and think at different levels of abstraction.
def calculate_average(grades):
"""Return the average of a list of grades."""
if not grades:
return 0.0
return sum(grades) / len(grades)
Parameters are the names in the function definition. Arguments are the values you pass when calling the function. This distinction comes up in exams.
Variables defined inside a function are local - they don't exist outside. This is a major source of confusion:
def add_one(x):
x = x + 1
return x
n = 5
result = add_one(n)
print(n) # Still 5 - n was not modified
print(result) # 6
But with mutable objects (lists, dicts), the function CAN modify the original:
def add_item(lst):
lst.append("new")
my_list = ["a", "b"]
add_item(my_list)
print(my_list) # ["a", "b", "new"] - modified!
This is because lists are passed by reference. The function gets the same list object, not a copy.
return sends a value back to the caller. print displays text on screen. They are completely different. A function that prints but doesn't return gives you None:
def bad_add(a, b):
print(a + b) # Displays 7 but returns None
result = bad_add(3, 4)
print(result) # None!
Ordered, mutable sequences. The workhorse of Python programming.
grades = [85, 92, 78, 95, 88]
# Indexing (0-based)
grades[0] # 85 (first)
grades[-1] # 88 (last)
# Slicing [start:stop:step]
grades[1:3] # [92, 78]
grades[::-1] # [88, 95, 78, 92, 85] (reversed)
# Common methods
grades.append(91) # Add to end
grades.sort() # Sort in place
grades.remove(78) # Remove first occurrence
len(grades) # Length
Key-value pairs. O(1) average lookup - use these when you need to look things up by name.
student = {
"name": "Alice",
"id": 1234567,
"courses": ["CSCA08", "MATA31"]
}
# Access
student["name"] # "Alice"
student.get("gpa", 0.0) # 0.0 (default if key missing)
# Iterate
for key, value in student.items():
print(f"{key}: {value}")
A concise way to create lists. Exams love these.
# Squares of even numbers 0-9
squares = [x**2 for x in range(10) if x % 2 == 0]
# [0, 4, 16, 36, 64]
# Equivalent to:
squares = []
for x in range(10):
if x % 2 == 0:
squares.append(x**2)
The concept that makes or breaks intro CS students. Recursion is when a function calls itself to solve a smaller version of the same problem.
Every recursive function needs:
def factorial(n):
# Base case
if n <= 1:
return 1
# Recursive case
return n * factorial(n - 1)
# factorial(4) = 4 * factorial(3)
# = 4 * 3 * factorial(2)
# = 4 * 3 * 2 * factorial(1)
# = 4 * 3 * 2 * 1
# = 24
Don't try to trace every recursive call in your head. Instead, trust the function. Assume the recursive call works correctly for the smaller case. Then ask: if I had the answer to the smaller problem, how would I build the answer to the full problem? That's the recursive step.
These appear on nearly every intro CS exam:
fib(n) = fib(n-1) + fib(n-2)def flatten(nested):
"""Flatten a nested list."""
result = []
for item in nested:
if isinstance(item, list):
result.extend(flatten(item)) # Recurse
else:
result.append(item)
return result
flatten([1, [2, [3, 4]], 5]) # [1, 2, 3, 4, 5]
OOP is about organizing code around objects that combine data (attributes) and behavior (methods).
class Student:
def __init__(self, name, student_id):
self.name = name
self.student_id = student_id
self.courses = []
self.grades = {}
def enroll(self, course):
if course not in self.courses:
self.courses.append(course)
def add_grade(self, course, grade):
self.grades[course] = grade
def gpa(self):
if not self.grades:
return 0.0
return sum(self.grades.values()) / len(self.grades)
def __str__(self):
return f"{self.name} ({self.student_id})"
__init__ - constructor, called when you create an instanceself - reference to the current instance (must be first parameter)class GradStudent(Student)__str__ - what print(obj) shows__repr__ - the "official" string representation__eq__ - defines what == means for your objectsclass GradStudent(Student):
def __init__(self, name, student_id, advisor):
super().__init__(name, student_id)
self.advisor = advisor
self.thesis_title = None
super().__init__(...) calls the parent's constructor. GradStudent has everything Student has, plus advisor and thesis_title.
Off-by-one errors: range(5) gives [0,1,2,3,4], not [0,1,2,3,4,5]. The stop value is exclusive. If you need 1 to 5, use range(1, 6).
Mutating a list while iterating: Never do this:
# WRONG - skips elements
for item in my_list:
if should_remove(item):
my_list.remove(item)
# RIGHT - iterate over a copy, or build a new list
result = [item for item in my_list if not should_remove(item)]
Forgetting to return: If a function doesn't explicitly return, it returns None. This is the #1 source of mysterious None values.
Mutable default arguments:
# WRONG - the default list is shared across all calls
def add_item(item, lst=[]):
lst.append(item)
return lst
# RIGHT - use None as default
def add_item(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
String immutability: Strings can't be modified in place. s[0] = "X" raises an error. Create a new string instead: s = "X" + s[1:].
Koa is an AI tutor that walks you through problems step by step, asks checkpoint questions to make sure you understand, and adapts when you're stuck. It's like having a TA available 24/7.
Try Koa Freeprint(variable_name) at key points to see what your variables actually contain (vs what you think they contain). For more complex bugs, use Python's built-in debugger: add breakpoint() where you want to pause, then step through line by line. Most importantly: read the error message. Python's error messages tell you exactly what went wrong and which line it happened on.