Loading…
Python variables don't need a declared type — the type lives on the value, not the variable name. You just assign and go.
count = 5 # int
price = 4.99 # float
name = "FeatCode" # str
is_ready = True # bool
nothing = None # Python's nullThe four types you'll use constantly in DSA problems are int, str, list, and dict — bool and float show up too, but far less often.
Arithmetic mostly looks like every other language, with two Python-specific operators worth knowing cold: // (integer/floor division) and ** (exponent).
7 / 2 # 3.5 (true division, always returns a float)
7 // 2 # 3 (floor division, returns an int)
7 % 2 # 1 (modulo — remainder)
2 ** 10 # 1024 (exponent)
-7 // 2 # -4 (floor division always rounds toward negative infinity!)That last line trips people up: floor division on a negative number rounds down (more negative), not toward zero. If you need "round toward zero" behavior, use int(-7 / 2) instead.
Comparison chaining is a nice Python-only shortcut:
if 0 <= i < len(nums): # equivalent to: 0 <= i and i < len(nums)
...
Strings are immutable sequences of characters — indexable and sliceable just like lists, but you can never modify one in place. Any "change" builds a new string.
s = "leetcode"
s[0] # 'l'
s[-1] # 'e' (negative index = from the end)
s[2:5] # 'etc' (slice: start inclusive, end exclusive)
s[::-1] # 'edocteel' (reverse — step of -1)
len(s) # 8
s.upper() # 'LEETCODE'
s.lower() # 'leetcode'
s + "!" # 'leetcode!' (concatenation, builds a new string)Because strings are immutable, repeatedly concatenating one inside a loop is O(n²) — each += rebuilds the whole string. For heavy string-building, collect pieces in a list and join at the end:
parts = []
for c in "abc":
parts.append(c.upper())
result = "".join(parts) # 'ABC' — O(n), not O(n^2)Splitting and joining are the two you'll reach for constantly:
"a,b,c".split(",") # ['a', 'b', 'c']
"hello world".split() # ['hello', 'world'] (splits on whitespace by default)
"-".join(["a","b"]) # 'a-b'
A list is Python's dynamic array — the workhorse data structure for almost every array-based problem. Unlike strings, lists are mutable.
nums = [3, 1, 4, 1, 5]
nums.append(9) # [3, 1, 4, 1, 5, 9] — add to the end, O(1) amortized
nums.pop() # removes & returns last item, O(1)
nums.pop(0) # removes & returns item at index 0, O(n) — avoid in loops
nums.insert(0, 100) # insert at index, O(n)
nums.remove(1) # removes the FIRST value equal to 1, O(n)
3 in nums # membership check, O(n) for a list
nums.sort() # sorts in place, ascending, O(n log n)
nums.sort(reverse=True) # descending
sorted(nums) # returns a NEW sorted list, leaves nums untouchedSlicing works exactly like strings, and is one of the most-used list features in these problems:
nums[1:3] # elements at index 1 and 2
nums[:2] # first two elements
nums[-2:] # last two elements
nums[::2] # every other elementTwo pitfalls worth knowing before they bite you:
A tuple is an immutable list — same indexing/slicing, but you can never change it after creation. That immutability is exactly why tuples are useful as dictionary keys and set elements, which lists can never be.
point = (3, 4)
point[0] # 3
x, y = point # unpacking: x=3, y=4
seen = set()
seen.add((0, 0)) # fine — tuples are hashable
# seen.add([0, 0]) # TypeError — lists are NOT hashableA set stores unique values with O(1) average membership checks — reach for one anytime you need "have I seen this before?" without caring about order or counts.
seen = set()
seen.add(5)
seen.add(5) # no-op, already present
5 in seen # True, O(1) average
seen.remove(5) # raises if missing
seen.discard(5) # no error if missing
a = {1, 2, 3}
b = {2, 3, 4}
a & b # {2, 3} intersection
a | b # {1,2,3,4} union
a - b # {1} difference
A dict maps keys to values with O(1) average lookup, insert, and delete — this is the single most important data structure for the Arrays & Hashing pattern that opens almost every DSA roadmap.
count = {}
count["a"] = 1
count["a"] += 1 # 2
count.get("z") # None — no KeyError, unlike count["z"]
count.get("z", 0) # 0 — default if missing
"a" in count # True — checks KEYS, O(1) average
for key in count: # iterates keys
...
for key, val in count.items(): # iterates key/value pairs
...The single most common bug in beginner solutions is count[key] += 1 on a key that doesn't exist yet — that raises KeyError. Either check first, or use .get() with a default, or reach for collections.defaultdict (covered in lesson 10).
# Safe pattern without defaultdict:
count[c] = count.get(c, 0) + 1
if/elif/else works as you'd expect — the one real difference from C-family languages is that Python uses indentation instead of braces to mark blocks. Get the indentation wrong and you get a real error, not just ugly code.
if n < 0:
sign = -1
elif n == 0:
sign = 0
else:
sign = 1Python has no switch/case statement (older versions, at least) — a chain of elif, or a dict mapping values to actions, covers the same need.
Falsy values worth memorizing — these all evaluate as False in an if: 0, 0.0, "" (empty string), [] (empty list), {} (empty dict), set() (empty set), and None.
nums = []
if not nums: # True — empty list is falsy
print("empty")
for loops in Python iterate over items directly, not index counters — range(), enumerate(), and zip() are how you bring indices or multiple sequences into the loop when you need them.
for x in [10, 20, 30]:
print(x) # 10, then 20, then 30
for i in range(5):
print(i) # 0, 1, 2, 3, 4
for i in range(2, 10, 2):
print(i) # 2, 4, 6, 8 (start, stop, step)
for i, x in enumerate([10, 20, 30]):
print(i, x) # 0 10 / 1 20 / 2 30
for a, b in zip([1,2,3], [4,5,6]):
print(a, b) # 1 4 / 2 5 / 3 6while loops are the natural fit for two-pointer and sliding-window patterns, where the stopping condition depends on pointer positions rather than a fixed count:
l, r = 0, len(nums) - 1
while l < r:
if nums[l] + nums[r] == target:
break
elif nums[l] + nums[r] < target:
l += 1
else:
r -= 1break exits the loop entirely; continue skips to the next iteration. Both work the same as every other language.
Every solution you write on FeatCode is a method — a function that lives inside a class and takes self as its first parameter. Here's the anatomy:
class Solution:
def twoSum(self, nums, target):
seen = {}
for i, n in enumerate(nums):
need = target - n
if need in seen:
return [seen[need], i]
seen[n] = i
return []self refers to the instance the method is called on — you never pass it explicitly, Python fills it in automatically when you call solution.twoSum(nums, target).
A function without an explicit return implicitly returns None — a common source of "why is my output None" bugs when a return is accidentally placed inside the wrong branch or indentation level.
Default arguments and *args/**kwargs come up in interview-style code occasionally:
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
greet("Sam") # 'Hello, Sam!'
greet("Sam", "Hi") # 'Hi, Sam!'
A comprehension builds a list/dict/set in one line instead of a multi-line loop with .append() calls. They're not just shorter — they're also generally faster than the equivalent explicit loop.
squares = [x * x for x in range(10)]
evens = [x for x in range(10) if x % 2 == 0]
pairs = [(x, y) for x in range(3) for y in range(3)]
square_map = {x: x * x for x in range(5)} # dict comprehension
unique_lens = {len(w) for w in ["a", "bb", "cc"]} # set comprehensionRule of thumb: reach for a comprehension when the loop body is a single expression. The moment you need multiple statements, branching logic, or side effects, switch back to a normal loop for readability.
You'll write classes yourself for any "design" problem — LRU Cache, Min Stack, Trie, and similar problems all ask you to build a small stateful object with a constructor and a handful of methods.
class MinStack:
def __init__(self):
self.stack = []
self.min_stack = []
def push(self, val):
self.stack.append(val)
m = min(val, self.min_stack[-1]) if self.min_stack else val
self.min_stack.append(m)
def pop(self):
self.stack.pop()
self.min_stack.pop()
def get_min(self):
return self.min_stack[-1]__init__ is the constructor, called automatically when you write MinStack(). Every method — including __init__ — takes self first, and you access instance data through self.attribute_name, both to read and to write it.
A handful of built-in functions and standard-library imports cover the overwhelming majority of what shows up in DSA solutions:
len(x) # length of a list/string/dict/set
min(x), max(x) # smallest/largest — works on any iterable
sum(x) # sum of a list of numbers
abs(x) # absolute value
sorted(x) # new sorted list
sorted(x, key=len) # sort by a custom key function
sorted(x, reverse=True) # descending
list(reversed(x)) # reversed as a listFrom the collections module — these three come up in a huge fraction of Blind 75/150 solutions:
from collections import Counter, defaultdict, deque
Counter("aabbbc") # Counter({'b': 3, 'a': 2, 'c': 1}) — counts every element in one call
d = defaultdict(list)
d["key"].append(1) # no KeyError — missing keys auto-create an empty list
q = deque([1, 2, 3])
q.appendleft(0) # O(1) — a plain list.insert(0, x) is O(n)
q.popleft() # O(1) — a plain list.pop(0) is O(n)And heapq for anything involving a "smallest/largest k" or priority-queue pattern — Python only has a min-heap built in, so negate values to simulate a max-heap:
import heapq
heap = []
heapq.heappush(heap, 5)
heapq.heappush(heap, 1)
heapq.heappop(heap) # 1 — always pops the smallest
# max-heap trick: push negated values
heapq.heappush(heap, -5)
-heapq.heappop(heap) # largest original value