Skip to content
1stepGrowLearnCompareGrow

Python Basics: Names, Collections, Control Flow and Comprehensions

Python basics with runnable examples: names and objects, lists, dicts and sets, control flow and comprehensions, plus the traps behind most beginner bugs.

Ayushi Kulshreshta

AI Engineer

10 min readUpdated
Share
On this page

Python basics come down to a small set of pieces: names bound to objects, four core collections (list, tuple, dict and set), a few control-flow statements, and comprehensions. Get these right early, and the rest of the language becomes considerably less surprising.

Most beginner bugs come from misreading those pieces rather than from anything advanced. [[0] * 3] * 3 builds three references to one list, {} makes a dict rather than a set, and nums = nums.sort() throws your list away. This guide is for new Python programmers, including those heading into data work. It explains the model behind each piece so those bugs make sense, with runnable examples and a quick reference for choosing a collection.

This guide is the first stop in a short series. Once you are comfortable here, the natural follow-ups are Python numeric data types, strings, operators and user-defined functions.

How do Python variables actually work?

A variable is a name bound to an object, not a container holding a value. That one mental model explains most of the confusing behaviour beginners meet.

Python
a = [1, 2, 3]
b = a              # b is another name for the SAME list
b.append(4)
print(a)           # [1, 2, 3, 4]

Nothing was copied. Instead, there are two names and one object.

For immutable objects, however, this never causes trouble, because you cannot modify them in place:

Python
x = 5
y = x
y += 1       # rebinds y to a new int
print(x)     # 5

When you want independence, you therefore have to ask for a copy:

Python
a = [[1, 2], [3, 4]]
b = a.copy()          # shallow: new outer list, same inner lists
b = a[:]              # same thing

import copy
b = copy.deepcopy(a)  # recursive, for nested mutable structures

Shallow copies are the usual gotcha, because they share anything nested:

Python
grid = [[0, 0], [0, 0]]
shallow = grid.copy()
shallow[0][0] = 9
print(grid)      # [[9, 0], [0, 0]]  - inner lists are shared

The classic multiplication trap follows from the same rule:

Python
rows = [[0] * 3] * 3      # BROKEN - three references to ONE list
rows[0][0] = 1
print(rows)               # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]

rows = [[0] * 3 for _ in range(3)]    # correct - three distinct lists

The copy module documentation spells out exactly what shallow and deep copies duplicate.

Which Python data structures should you learn first?

Learn the four built-in collections first, because they sit at the heart of Python basics and nearly every program uses them. The official data structures tutorial covers each in more depth.

List — ordered, mutable, allows duplicates:

Python
nums = [3, 1, 4, 1, 5]
nums.append(9)
nums.insert(0, 0)
nums.remove(1)        # removes the FIRST 1
nums.pop()            # removes and returns the last: 9
nums.sort()           # in place, returns None
print(nums)           # [0, 1, 3, 4, 5]
sorted(nums, reverse=True)    # a new list: [5, 4, 3, 1, 0]

Tuple — ordered, immutable, hashable. Our Python tuples guide covers unpacking and named tuples in depth.

Dictionary — key-value lookup, insertion-ordered since Python 3.7:

Python
person = {"name": "Ananya", "age": 28}

person["city"] = "Pune"
person.get("phone")                # None instead of KeyError
person.get("phone", "unknown")     # 'unknown'
person.setdefault("tags", []).append("new")

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

The ordering guarantee is documented under mapping types in the standard library reference. Merging and comprehensions are also worth knowing:

Python
defaults = {"retries": 3, "timeout": 30}
config = defaults | {"timeout": 60}        # Python 3.9+
print(config)                              # {'retries': 3, 'timeout': 60}

squares = {n: n**2 for n in range(5)}      # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

Set — unordered, unique, fast membership:

Python
tags = {"python", "data", "python"}
print(len(tags))    # 2 - the duplicate is dropped; display order is not guaranteed

tags.add("ml")
other = {"data", "sql"}
tags & other        # {'data'} - intersection
tags | other        # union
tags - other        # difference
tags ^ other        # symmetric difference

The performance point is the one that matters in practice:

Python
big_list = list(range(1_000_000))
big_set = set(big_list)

999_999 in big_list     # O(n) - scans
999_999 in big_set      # O(1) on average - hashes

Python's own TimeComplexity page lists these costs for CPython. So if you test membership repeatedly against the same collection, convert it to a set once. It is one of the easiest large speedups available.

List vs dict vs set: a quick reference

You need Use Example
Order, and to modify it list rows to process
A fixed record, or a dict key tuple (lat, lon)
Lookup by key dict user ID to profile
Uniqueness or fast membership set IDs already seen

How does control flow work in Python?

Control flow in Python is if/elif/else, for and while loops, and, since Python 3.10, match.

Python
score = 82
if score >= 90:
    grade = "A"
elif score >= 75:
    grade = "B"
else:
    grade = "C"
print(grade)    # B

Structural pattern matching is useful for dispatching on the shape of data, as the match statement tutorial shows:

Python
def dispatch(command):
    match command.split():
        case ["quit"]:
            return "bye"
        case ["load", filename]:
            return f"loading {filename}"
        case ["set", key, value]:
            return f"{key} = {value}"
        case _:
            raise ValueError(f"Unknown command: {command}")

dispatch("load sales.csv")    # 'loading sales.csv'

Loop idioms such as enumerate and zip are worth internalising:

Python
names = ["Ananya", "Rohit"]
scores = [91, 78]

for i, name in enumerate(names, start=1):    # index and value
    print(i, name)
for name, score in zip(names, scores):       # parallel iteration
    print(name, score)

Iterating a list by index — for i in range(len(items)) — is nearly always a sign that enumerate or zip is the better tool.

The else clause on loops is obscure, yet occasionally perfect:

Python
for n in [3, 5, 7]:
    if n % 2 == 0:
        break
else:
    print("no even number found")     # runs only if the loop never broke

When should you use a comprehension instead of a loop?

Use a comprehension to build a list, dict or set from another iterable in one readable line, and switch to a loop once the logic needs nesting plus conditions. A comprehension is the idiomatic way to build a collection:

Python
squares = [x**2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]
pairs = [(x, y) for x in "ab" for y in [1, 2]]
# [('a', 1), ('a', 2), ('b', 1), ('b', 2)]

emails = ["a@example.com", "b@example.org", "c@example.com"]
unique_domains = {email.split("@")[1] for email in emails}   # a set of 2 domains
lazy = (x**2 for x in range(1_000_000))     # generator - computed on demand

That last line is a generator expression; iterators and generators explains why it costs almost no memory.

Know when to stop, though. Once there is nested filtering and a conditional expression, a loop is clearer:

Python
# too dense
result = [transform(x) if x.valid else fallback(x)
          for group in groups for x in group if x.enabled]

# clearer
result = []
for group in groups:
    for x in group:
        if not x.enabled:
            continue
        result.append(transform(x) if x.valid else fallback(x))

Readability is the point of comprehensions; past a certain complexity, they stop delivering it.

What counts as True in Python?

Every object has a truth value. Empty collections, zero, "" and None are falsy; almost everything else is truthy. The full list is in the truth value testing reference.

Python
bool([])        # False - empty sequences are falsy
bool([0])       # True  - non-empty, regardless of contents
bool(0)         # False
bool("")        # False
bool(None)      # False

As a result, the idiomatic emptiness check is:

Python
items = []
if not items:
    print("nothing to do")      # good
if len(items) == 0:
    print("nothing to do")      # works, less idiomatic

But be careful with values where 0 is meaningful — see the default-value caveat in our Python operators guide.

Common mistakes with Python basics

These are the errors that come up again and again in beginner code. Each one follows from the rules above.

  • {} is an empty dict, not an empty set. Use set() for an empty set; type({}) is <class 'dict'>.
  • list.sort() returns None. Writing nums = nums.sort() throws your list away. Use sorted(nums) when you want a new list back.
  • Modifying a list while looping over it. Removing items mid-loop skips elements. Instead, build a new list with a comprehension.
  • Shared inner lists. [[0] * 3] * 3 and shallow copies both share nested objects, as shown earlier.
  • Mutable default arguments. def f(items=[]) shares one list across calls; the functions guide shows the fix.

For example, here is the loop-removal bug in action:

Python
nums = [1, 2, 2, 3]
for n in nums:
    if n == 2:
        nums.remove(n)
print(nums)                            # [1, 2, 3] - one 2 survived

nums = [1, 2, 2, 3]
nums = [n for n in nums if n != 2]
print(nums)                            # [1, 3]

What should you learn after Python basics?

With these Python basics in place, the next steps are numeric types, strings, functions and exception handling. After that, move on to the data stack, starting with NumPy and then pandas.

The Sunday Growth Brief

One email a week: the best new comparisons, a fresh roadmap and the tech news worth your attention.

No spam. Unsubscribe in one click.

Lists vs tuples in Python and Python strings go deeper on individual types.

Frequently asked questions

What are the core data structures in Python?

Lists for ordered mutable sequences, tuples for immutable records, dictionaries for key-value lookup, and sets for membership and uniqueness. Almost everything else in everyday Python, from JSON handling to pandas indexes, is built from or modelled on these four, so learning how each one behaves pays off for years.

Why did changing one list change another?

Because assignment binds a name to the same object rather than copying it. After b = a, both names refer to one list, so a change through either name shows up in both. Use a.copy() for a shallow copy, or copy.deepcopy(a) when the contents are themselves mutable, such as a list of lists.

When should I use a set instead of a list?

When you need uniqueness or fast membership testing. Checking x in some_set is O(1) on average, while x in some_list is O(n), so the difference becomes enormous at scale. The trade-off is that sets have no guaranteed order, cannot be indexed, and cannot hold unhashable items such as lists.

Are dictionaries ordered in Python?

Yes. Since Python 3.7, dictionaries preserve insertion order as a language guarantee, not just a CPython implementation detail. Iterating a dict, or calling list() on it, returns keys in the order you added them. Updating an existing key keeps its original position, while deleting and re-adding a key moves it to the end.

Written by

Ayushi Kulshreshta

AI Engineer

AI engineer building retrieval-augmented systems, previously a software developer at VDB Inc. and an R&D associate at Nokia.

Retrieval-Augmented GenerationLangChainPythonMachine learning

Related reading

Free weekly newsletter

Get the shortlist before everyone else

Every Sunday we send one email with the week’s sharpest course comparison, a career roadmap worth stealing, and the tech news and hiring signals we’re watching.

  • No fluff, ever
  • Unsubscribe anytime
  • 5-minute read

We never share your address. One click to leave.