Days 1–4, before 01-foundations. This file assumes you have never written a line of Python. Everything after it assumes you have read this one.
It is not a full Python course. It is exactly the Python you need to write algorithms and backend code, and nothing else.
Two ways. You need both.
The REPL — an interactive prompt. Type an expression, press Enter, see the answer immediately. This is where you experiment.
python>>> 2 + 2
4
>>> "hello".upper()
'HELLO'
>>> exit()
The REPL prints the value of whatever you type. That's why 2 + 2 shows 4 without you asking.
A script — a .py file you run start to finish. This is where real work lives.
python myfile.pyIn a script, nothing prints unless you say print(...). That difference catches everyone once.
2 + 2 # in a script: computes 4, throws it away, shows nothing
print(2 + 2) # shows 4Comments start with #. Python ignores the rest of the line. Use them to explain why, not what.
x = 86400 # seconds in a dayA variable is a name pointing at a value. No declaration keyword, no type annotation required.
name = "Harshit"
age = 22
height = 5.9
is_student = True= means assign, not "equals". x = 5 says "make x point at 5." To test equality you use ==, which is a different thing entirely and a classic beginner bug.
Python is dynamically typed — a variable can point at anything, and can change:
x = 5 # x is an int
x = "five" # now x is a str. Legal, though usually a bad idea.Check a type with type():
type(5) # <class 'int'>
type(5.0) # <class 'float'>
type("5") # <class 'str'>
type(True) # <class 'bool'>
type([1, 2]) # <class 'list'>
type(None) # <class 'NoneType'>The core types you'll use constantly:
| Type | Example | What it's for |
|---|---|---|
int |
42, -7, 0 |
whole numbers — array indices, counts |
float |
3.14, -0.5 |
decimals — averages, distances |
str |
"hello" |
text |
bool |
True, False |
conditions (capital T and F — true is an error) |
list |
[1, 2, 3] |
ordered, changeable sequence |
tuple |
(1, 2) |
ordered, unchangeable sequence |
dict |
{"a": 1} |
key → value lookup |
set |
{1, 2, 3} |
unordered unique values |
None |
None |
"no value" — a real value meaning absence |
Naming rules: letters, digits, underscores; can't start with a digit; case-sensitive. Python convention is snake_case for variables and functions, PascalCase for classes, SCREAMING_CASE for constants.
total_count = 0 # good
totalCount = 0 # works, but not Python style
2fast = 0 # SyntaxErrorConverting between types:
int("42") # 42 — string to int
int(3.9) # 3 — TRUNCATES, does not round
str(42) # "42"
float("3.14") # 3.14
bool(0) # False
list("abc") # ['a', 'b', 'c']int("hello") raises a ValueError. Conversion only works when the content makes sense.
7 + 3 # 10 addition
7 - 3 # 4 subtraction
7 * 3 # 21 multiplication
7 / 3 # 2.333... TRUE division — always gives a float
7 // 3 # 2 FLOOR division — drops the remainder, gives an int
7 % 3 # 1 modulo — the REMAINDER
7 ** 3 # 343 exponent (7 cubed)// and % matter enormously in algorithms. Learn them properly.
// is how you find a midpoint: mid = (lo + hi) // 2. You want an integer index, not 3.5.
% is how you detect divisibility and how you wrap around:
n % 2 == 0 # is n even?
n % 3 == 0 # is n divisible by 3?
(i + 1) % len(arr) # next index, wrapping to 0 at the end — circular arrays
hour % 12 # clock arithmeticA trap worth knowing now: // rounds toward negative infinity, not toward zero.
7 // 2 # 3
-7 // 2 # -4 ← not -3
int(-7 / 2) # -3 ← truncates toward zeroBoth behaviours are correct; they're just different. Know which one your problem wants.
Shorthand assignment:
x = 5
x += 3 # same as x = x + 3 → 8
x -= 2 # 6
x *= 2 # 12
x //= 5 # 2Python has no ++. Use x += 1.
Useful built-ins:
abs(-5) # 5
min(3, 7, 2) # 2
max([3, 7, 2]) # 7 — works on a list too
sum([1, 2, 3]) # 6
round(3.7) # 4
round(3.14159, 2) # 3.14
divmod(17, 5) # (3, 2) — quotient and remainder togetherIntegers in Python have no size limit. 2 ** 1000 just works. In Java or C++ that overflows. Mention this in interviews when a problem says "assume 32-bit."
Text. Single or double quotes, your choice — be consistent.
s = "hello world"Indexing — position, starting at 0. Negative counts from the end.
s[0] # 'h' first
s[4] # 'o' fifth
s[-1] # 'd' LAST — you'll use this constantly
s[-2] # 'l' second to lastSlicing — s[start:stop], where stop is excluded.
s[0:5] # 'hello' indices 0,1,2,3,4 — NOT 5
s[:5] # 'hello' omit start = from the beginning
s[6:] # 'world' omit stop = to the end
s[:] # whole thing (a copy)
s[::2] # 'hlowrd' every 2nd character
s[::-1] # 'dlrow olleh' REVERSED — memorise this oneThe "stop is excluded" rule is universal in Python (slices, range, everything). It feels wrong for a week, then feels obvious forever. The payoff: len(s[a:b]) == b - a.
Common methods:
s.upper() # 'HELLO WORLD'
s.lower()
s.strip() # remove whitespace from both ends
s.split() # ['hello', 'world'] — splits on whitespace
"a,b,c".split(",") # ['a', 'b', 'c']
"-".join(["a","b"]) # 'a-b' — the REVERSE of split
s.replace("l", "L") # 'heLLo worLd'
s.startswith("hello") # True
s.find("world") # 6 — index, or -1 if absent
s.count("l") # 3
len(s) # 11
"lo" in s # True — substring checkCharacter tests — used constantly in parsing problems:
"a".isalpha() # True letter?
"1".isdigit() # True digit?
"a1".isalnum() # True letter or digit?
" ".isspace() # True
"a".islower() # TrueCharacters and numbers:
ord('a') # 97 character → its number
chr(97) # 'a' number → character
ord('c') - ord('a') # 2 letter → 0-based alphabet indexThat last line is the standard trick for "count the letters" problems: it maps a-z onto 0-25 so you can use a 26-slot list.
Strings are immutable. You cannot change one in place.
s = "hello"
s[0] = "H" # TypeError!
s = "H" + s[1:] # this works — it makes a NEW stringTo modify heavily, convert to a list, edit, join back:
chars = list("hello")
chars[0] = "H"
s = "".join(chars) # 'Hello'f-strings — the modern way to build strings with values in them:
name = "Harshit"
count = 3
print(f"{name} solved {count} problems") # Harshit solved 3 problems
print(f"{count * 2}") # expressions work inside
print(f"{3.14159:.2f}") # 3.14 — 2 decimal placesThe f before the quote is required. Without it you get the literal text {name}.
5 == 5 # True EQUAL — two equals signs
5 != 3 # True not equal
5 > 3 # True
5 >= 5 # TrueCombining conditions:
x = 7
x > 5 and x < 10 # True — both must hold
x > 5 or x > 100 # True — at least one
not (x > 5) # False — flips it
5 < x < 10 # True — chaining! Python allows this, most languages don'tTruthiness — every value is usable as a condition. These are "falsy":
False, None, 0, 0.0, "", [], {}, set(), ()Everything else is "truthy". So:
if my_list: # "if the list is not empty"
if not my_string: # "if the string is empty"
if count: # "if count is not zero"This is idiomatic Python. Write if my_list:, not if len(my_list) > 0:.
== vs is: == compares values; is compares identity (same object in memory).
a = [1, 2]
b = [1, 2]
a == b # True — same contents
a is b # False — two different list objectsOnly use is for None: if x is None:. Using is for numbers or strings sometimes appears to work due to caching, then breaks. That's a real bug source.
The workhorse. An ordered, changeable sequence.
nums = [3, 1, 4, 1, 5]
mixed = [1, "two", 3.0, True] # legal, rarely wise
empty = []Access and slice — same rules as strings:
nums[0] # 3
nums[-1] # 5
nums[1:3] # [1, 4]
nums[::-1] # [5, 1, 4, 1, 3] reversed copy
len(nums) # 5Unlike strings, lists are mutable:
nums[0] = 99 # [99, 1, 4, 1, 5]Methods — and their costs, which matter for algorithms:
nums.append(6) # add to END — O(1), fast
nums.pop() # remove from END — O(1), returns it
nums.pop(0) # remove from FRONT — O(n), SLOW
nums.insert(0, 9) # insert at FRONT — O(n), SLOW
nums.remove(4) # remove first value 4 — O(n)
nums.extend([7, 8]) # append several
nums.sort() # sort IN PLACE, returns None
sorted(nums) # returns a NEW sorted list
nums.reverse() # reverse in place
nums.index(4) # first index of 4 — O(n)
nums.count(1) # how many 1s — O(n)
4 in nums # True — O(n) scanTwo things to internalise now, because they cause slow code later:
append/popat the end are fast; anything at the front is slow. Adding at index 0 shifts every other element.x in some_listscans the whole list. Inside a loop, that's the classic accidental O(n²).
sort() vs sorted() trips up everyone:
nums.sort() # modifies nums, RETURNS None
result = nums.sort() # result is None! A very common bug.
result = sorted(nums) # this is what you wantedSorting with a rule:
words = ["ccc", "a", "bb"]
sorted(words, key=len) # ['a', 'bb', 'ccc']
sorted(words, reverse=True) # descending
pairs = [(1, 'z'), (2, 'a')]
sorted(pairs, key=lambda p: p[1]) # sort by second elementlambda p: p[1] is a small anonymous function: "given p, give me p[1]." You'll see it constantly.
Copying — the trap:
a = [1, 2, 3]
b = a # NOT a copy — b points at the SAME list
b.append(4)
print(a) # [1, 2, 3, 4] ← a changed too!
b = a[:] # a real (shallow) copy
b = list(a) # also a copy2-D lists (grids):
grid = [[0, 1], [2, 3]]
grid[0][1] # 1 — row 0, column 1
len(grid) # 2 — number of rows
len(grid[0]) # 2 — number of columns
# Building one — the CORRECT way:
grid = [[0] * 3 for _ in range(2)] # 2 rows, 3 cols, all zeros
# The WRONG way — a real bug you will hit:
grid = [[0] * 3] * 2 # all rows are the SAME list
grid[0][0] = 1 # sets it in EVERY rowThe second version creates one row and points at it twice. for _ in range(2) builds a genuinely new list each time.
Tuples — like lists, but immutable.
point = (3, 4)
point[0] # 3
point[0] = 9 # TypeError — cannot change
x, y = point # UNPACKING — x=3, y=4. Used everywhere.
a, b = b, a # swap two variables in one line, no temp neededUse a tuple when the values belong together and shouldn't change: coordinates, a (value, index) pair, a database row. Tuples can be dictionary keys; lists cannot.
Sets — unordered, no duplicates, and membership testing is O(1).
s = {1, 2, 3}
s = set([1, 2, 2, 3]) # {1, 2, 3} — duplicates dropped
empty = set() # NOT {} — that makes an empty dict
s.add(4)
s.remove(1) # KeyError if absent
s.discard(1) # silent if absent
2 in s # True — O(1), NOT a scan
len(s)Set operations:
a = {1, 2, 3}
b = {2, 3, 4}
a & b # {2, 3} intersection — in both
a | b # {1,2,3,4} union — in either
a - b # {1} difference — in a, not b
a ^ b # {1, 4} symmetric difference — in exactly oneWhy sets matter so much: x in list is O(n); x in set is O(1). Converting a list to a set before repeated lookups is one of the most common speedups in all of interview code.
seen = set(my_list) # O(n) once
if x in seen: # O(1) every time afterKey → value. The single most important structure in interview Python.
ages = {"harshit": 22, "alice": 30}
ages["harshit"] # 22
ages["bob"] # KeyError! — the key doesn't exist
ages.get("bob") # None — safe
ages.get("bob", 0) # 0 — safe, with a defaultModifying:
ages["bob"] = 25 # add or overwrite
del ages["bob"] # remove
ages.pop("bob", None) # remove, with a default if missing
"alice" in ages # True — checks KEYS, O(1)
len(ages)Iterating:
for key in ages: # keys
print(key)
for key, value in ages.items(): # BOTH — use this most
print(key, value)
for value in ages.values():
print(value)The counting pattern — you'll write this a hundred times:
counts = {}
for c in "hello":
counts[c] = counts.get(c, 0) + 1
# {'h':1, 'e':1, 'l':2, 'o':1}Read it as: "take the current count, or 0 if there isn't one, add 1, store it back."
Grouping:
groups = {}
for word in ["apple", "avocado", "banana"]:
first = word[0]
if first not in groups:
groups[first] = []
groups[first].append(word)
# {'a': ['apple', 'avocado'], 'b': ['banana']}Both of these have shorter forms using Counter and defaultdict — those are in 01 §1.5. Learn the manual version first so you understand what the shortcut does.
Keys must be immutable. Strings, numbers, and tuples work. Lists do not.
d[(1, 2)] = "ok" # fine — tuple key
d[[1, 2]] = "no" # TypeError: unhashable type: 'list'score = 85
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
else:
grade = "F"Indentation is the syntax. Python has no braces. The indented block belongs to the if. Use 4 spaces, consistently — mixing tabs and spaces is an error.
if x > 5:
print("big") # inside the if
print("always") # outside — runs regardlesselif is checked only if everything above it was False. Once one branch matches, the rest are skipped.
Ternary — a one-line conditional value:
status = "pass" if score >= 60 else "fail"for over a sequence:
for x in [1, 2, 3]:
print(x)
for c in "abc":
print(c)
for key, value in my_dict.items():
print(key, value)range — generates numbers. Same "stop excluded" rule.
range(5) # 0, 1, 2, 3, 4
range(2, 5) # 2, 3, 4
range(0, 10, 2) # 0, 2, 4, 6, 8 — step
range(5, 0, -1) # 5, 4, 3, 2, 1 — backwards
for i in range(len(nums)): # loop by INDEX
print(i, nums[i])enumerate — index and value together. Prefer it over range(len(...)):
for i, x in enumerate(nums):
print(f"index {i} holds {x}")zip — walk two sequences in parallel:
for name, age in zip(names, ages):
print(name, age)while — repeat while a condition holds:
lo, hi = 0, 10
while lo < hi:
mid = (lo + hi) // 2
lo = mid + 1A while loop that never changes its condition runs forever. If your program hangs, that's usually why. Ctrl+C stops it.
break and continue:
for x in nums:
if x < 0:
continue # skip to the next iteration
if x > 100:
break # exit the loop entirely
print(x)The underscore means "I don't need this value":
for _ in range(3):
print("hi") # just repeat 3 timesA named, reusable block.
def greet(name):
return f"Hello, {name}"
message = greet("Harshit") # 'Hello, Harshit'defstarts the definitionnameis a parameter;"Harshit"is the argumentreturnsends a value back and exits immediately- No
returnmeans the function returnsNone
return exits right away. Everything after it in that call is dead:
def find(nums, target):
for i, x in enumerate(nums):
if x == target:
return i # exits the whole function, not just the loop
return -1 # only reached if the loop finishedThat shape — return inside the loop, a fallback after it — is the single most common function structure in interview code. Note the indentation: return -1 is at the function's level, not the loop's. Putting it inside the loop is a real bug (it would return on the first iteration).
Default parameters:
def greet(name, greeting="Hello"):
return f"{greeting}, {name}"
greet("Harshit") # 'Hello, Harshit'
greet("Harshit", "Hi") # 'Hi, Harshit'
greet(greeting="Hey", name="A") # keyword arguments, any orderThe mutable-default trap — memorise this one:
def bad(item, items=[]): # the SAME list is reused across ALL calls
items.append(item)
return items
bad(1) # [1]
bad(2) # [1, 2] ← not what anyone wants
def good(item, items=None): # the fix
if items is None:
items = []
items.append(item)
return itemsDefault values are evaluated once, when the function is defined — not on each call.
Returning multiple values (really a tuple):
def min_max(nums):
return min(nums), max(nums)
lo, hi = min_max([3, 1, 4]) # lo=1, hi=4Scope: variables made inside a function are local and vanish when it ends.
def f():
x = 10 # local
f()
print(x) # NameError — x doesn't exist out hereYou can read outer variables but not reassign them without nonlocal (enclosing function) or global (module level).
def outer():
count = 0
def inner():
nonlocal count # without this, count += 1 is an error
count += 1
inner()
return count # 1nonlocal shows up in tree problems where a helper updates a running best.
A compact way to build a list, set, or dict from a loop. Very common in Python code, so you must be able to read them even before you enjoy writing them.
# long form
squares = []
for x in range(5):
squares.append(x * x)
# comprehension — identical result
squares = [x * x for x in range(5)] # [0, 1, 4, 9, 16]Read it as: "x * x, for each x in range(5)." Expression first, loop second.
With a filter:
evens = [x for x in range(10) if x % 2 == 0] # [0,2,4,6,8]Set and dict versions:
{x * x for x in range(5)} # a set
{x: x * x for x in range(5)} # {0:0, 1:1, 2:4, ...}Rule of thumb: if it doesn't fit comfortably on one line, use a real loop. Nested comprehensions with multiple conditions are how Python code becomes unreadable.
A class is a template. An object is one thing built from that template.
class Dog:
def __init__(self, name, age): # the CONSTRUCTOR
self.name = name # an attribute
self.age = age
def bark(self): # a method
return f"{self.name} says woof"
d = Dog("Rex", 3)
d.name # 'Rex'
d.bark() # 'Rex says woof'__init__runs automatically when you create the objectselfis the object itself, and it is the first parameter of every method. You never pass it —d.bark()supplies it automaticallyself.name = namestores a value on this object
Names with double underscores like __init__ are called "dunder" methods — Python calls them for you at the right moment.
Why you need this for DSA: interview problems hand you classes.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = rightThose two show up in every linked list and tree problem in the curriculum. You mostly use them rather than write them, but you need to read node.next and node.left fluently.
Useful dunders:
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self): # how it prints
return f"Point({self.x}, {self.y})"
def __eq__(self, other): # how == works
return self.x == other.x and self.y == other.yWhen Python can't do something, it raises an exception and stops.
int("hello") # ValueError
[1,2][5] # IndexError
{"a":1}["b"] # KeyError
1 / 0 # ZeroDivisionError
"a" + 1 # TypeError
undefined_name # NameErrorHandling one:
try:
value = int(user_input)
except ValueError:
value = 0 # runs only if a ValueError happenedRaising one yourself:
def withdraw(balance, amount):
if amount > balance:
raise ValueError("insufficient funds")
return balance - amountDon't wrap everything in try/except. In algorithm code you usually want the crash — it tells you where your bug is.
This is a genuine skill and nobody teaches it. When your code fails, Python tells you exactly what happened. Read it bottom-up.
Traceback (most recent call last):
File "solution.py", line 12, in <module>
print(two_sum([1,2,3], 9))
File "solution.py", line 7, in two_sum
return [seen[need], i]
KeyError: 7
Read it like this:
- Last line first —
KeyError: 7is what went wrong: you asked a dict for key7, which isn't there. - The line above it —
return [seen[need], i]at line 7 is where. - Above that — the chain of calls that got you there. Line 12 called
two_sum, which failed at line 7.
Ninety percent of debugging is reading the last line and the last file location. Do that before changing anything.
The most common messages and what they actually mean:
| Message | Means |
|---|---|
IndentationError |
your spacing is inconsistent — mixed tabs and spaces, or a wrong-level line |
SyntaxError: invalid syntax |
usually a missing : or an unclosed bracket on the line above the one reported |
NameError: name 'x' is not defined |
typo, or used before assignment, or defined inside a function |
TypeError: 'NoneType' object is not subscriptable |
you indexed something that's None — often a function that forgot to return |
IndexError: list index out of range |
off-by-one; you used len(a) instead of len(a)-1 |
KeyError: 'x' |
that key isn't in the dict — use .get() |
TypeError: unhashable type: 'list' |
you used a list as a dict key or set element — use a tuple |
RecursionError |
infinite recursion, or a missing base case |
UnboundLocalError |
you assigned to a variable inside a function that also exists outside |
Bring in code from elsewhere.
import math
math.sqrt(16) # 4.0
from math import sqrt # import just one name
sqrt(16) # 4.0
from collections import Counter, defaultdict, deque
import heapqPut all imports at the top of the file. The ones you'll use for DSA are in 01 §1.5.
if __name__ == "__main__": — you'll see this everywhere, including in your practice files:
def solve():
return 42
if __name__ == "__main__":
print(solve())It means "only run this part when the file is executed directly, not when another file imports it." It lets a file be both a runnable script and an importable module.
Every beginner hits these. Knowing them in advance saves hours.
=instead of==in a condition.- Forgetting the colon at the end of
if,for,while,def,class. - Inconsistent indentation — always 4 spaces, never tabs.
nums.sort()returnsNone— usesorted(nums)if you want a value back.b = adoesn't copy a list — usea[:].[[0]*3]*3aliases the rows — use[[0]*3 for _ in range(3)].- Mutable default argument —
def f(x, items=[]). returninside a loop when you meant it after the loop — check your indentation.- Off-by-one from forgetting
stopis excluded —range(5)never yields 5. - Modifying a list while looping over it — build a new list instead.
x in some_listinside a loop — O(n²). Use a set.+=on a string in a loop — build a list and"".join(...)instead.
| Day | Read | Drill |
|---|---|---|
| 1 | §0.1–0.5: running Python, variables, types, numbers, strings, booleans | python -m drills.day0_python exercises 1–12 |
| 2 | §0.6–0.8: lists, tuples, sets, dicts | exercises 13–26 |
| 3 | §0.9–0.12: control flow, loops, functions, comprehensions | exercises 27–40 |
| 4 | §0.13–0.17: classes, errors, tracebacks, imports | exercises 41–50, then re-run everything |
Type every example in this file into the REPL as you read it. Do not read passively. Change a value and see what happens. Break it on purpose and read the traceback. That is the entire method.
Self-check before moving to 01-foundations. You should be able to, without looking:
- Explain the difference between
=and==, and between/and// - Reverse a string, and get its last character
- Write a loop that counts how many times each character appears in a string
- Explain why
[[0]*3]*3is broken - Write a function with a default parameter that returns two values
- Read a traceback and say which line failed and why
- Explain why
x in a_setis faster thanx in a_list
If any of those is shaky, redo that section's drills. There is no rush and no benefit to moving on early — 01 assumes all of it.
→ Next: 01 — Foundations