Free · No sign-in required

Basic Python for DSA

Loading…

12 short lessons, in order.

1. Variables & Basic Types

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 null

The 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.

  • type(x) tells you a value's type at runtime — handy when debugging.
  • Python is dynamically typed: the same variable can be reassigned to a different type later (rare in good code, but legal).
  • Integers in Python have no fixed size — no overflow to worry about, unlike Java/C++.

2. Operators & Expressions

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)
    ...

3. Strings

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'

4. Lists

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 untouched

Slicing 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 element

Two pitfalls worth knowing before they bite you:

  • list.pop(0) and list.insert(0, x) are O(n), not O(1) — if you're doing either in a loop, you probably want a collections.deque instead.
  • new_list = old_list copies the reference, not the data — both names point to the same list. Use new_list = old_list[:] or list(old_list) to actually copy.

5. Tuples & Sets

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 hashable

A 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

6. Dictionaries

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

7. Control Flow

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 = 1

Python 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")

8. Loops

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 6

while 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 -= 1

break exits the loop entirely; continue skips to the next iteration. Both work the same as every other language.

9. Functions

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!'

10. Comprehensions

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 comprehension

Rule 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.

11. Classes & Objects

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.

12. Built-ins & Modules You'll Use Constantly

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 list

From 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