Junior Programmer study materials
Junior Programmer study resources

Junior Programmer common mistakes

Junior Programmer · Understand the traps and how to avoid them.

Mistake 1

Writes `average = total / count` (or `7 / 2`) with two integer variables and then reports or asserts the wrong numeric result - e.g. expects 3.5 and the program produces 3 in Python 2, Java, C, C#, or Go.

The candidate learned division as a single mathematical operation and never internalized that the result type is decided by the operand types. The rule is stated once in the notes and then forgotten the moment the code compiles and runs.

What to do instead

Before writing any division, say out loud what the operand types are. If either operand must be fractional, force it at the point of division: total / float(count) or total / 2.0 in Python, (double) total / count in Java/C#, total / 2.0 in Go. Then check the boundary case by hand with small numbers (7 and 2) and confirm the printed type as well as the value.

Mistake 2

Loops over an array with the wrong bound - `for (i = 0; i <= len(arr); i++)` producing an index-out-of-range crash on the last iteration, or `i < len(arr) - 1` silently skipping the final element so a sum or max comes out short.

The candidate reasons about the loop in terms of "go through all the items" rather than in terms of the numeric range of valid indices, and writes the comparison operator from habit instead of deriving it from whether the language's length is a count or a last index.

What to do instead

For every index loop, write the valid index range first (0 to length-1 inclusive) and only then choose the operator. Sanity-check with a two-element list: trace the loop on paper and confirm exactly two iterations. Where the language allows it, use a for-each / for-in construct instead of an index, and keep the index form only when the index itself is needed.

Mistake 3

Defines a function such as `def add_item(item, bucket=[])` and is then surprised that calls accumulate state across invocations - the second call already contains the first call's item.

Default argument expressions are evaluated once, at function definition time, so the same mutable list or dict object is reused. The candidate reads the default as "a fresh empty list each call", which is the intuitive but incorrect reading.

What to do instead

Use an immutable sentinel and create the container inside the body: def add_item(item, bucket=None): if bucket is None: bucket = []. When reviewing any signature containing a default, ask what object that expression evaluates to and whether every call shares it.

Mistake 4

Writes a helper that is supposed to update a caller's value - `def increment(n): n = n + 1` - calls it, and then asserts that the original variable changed. In Python the same mistake reappears as `UnboundLocalError` after assigning to a name that also exists at module level.

The candidate conflates reassigning a parameter with mutating the object it points to. Pass-by-value-of-reference languages copy the reference into the new local name, so rebinding the local has no effect on the caller, while mutating the object (append, setitem) does.

What to do instead

Decide explicitly whether the function should return a new value or mutate a container. Prefer returning: return n + 1 and assign at the call site. If mutation is intended, mutate the object (bucket.append(x)) rather than rebinding the name. For module-level state, pass it in and return it, or use an explicit global/nonlocal declaration and know that you have now made the function harder to test.

Mistake 5

Removes elements inside a loop over the same list - `for item in items: if bad(item): items.remove(item)` - and ends up with leftover bad elements because consecutive items are skipped.

The loop's internal index advances on every iteration while the removal shifts all later elements one position left, so the element that moves into the current slot is never examined. This is invisible on test data where bad items never sit next to each other.

What to do instead

Build a new list with a comprehension or filter (items = [x for x in items if not bad(x)]), or iterate backwards over explicit indices when in-place removal is genuinely required. When testing, always include two adjacent invalid elements - a single invalid element will pass either way.

Mistake 6

Defines a subclass with its own `__init__` that sets the new attributes but never calls the parent constructor, then hits an AttributeError or missing-field bug when an inherited method touches state the parent was supposed to initialise.

The candidate treats inheritance as "copy the parent's methods in" and does not realise the parent's initialisation is a separate step that must be invoked explicitly. Constructor calls are also easy to omit silently because many languages do not warn.

What to do instead

Make super().__init__(...) (or the language's equivalent) the first statement of every subclass constructor, passing the arguments the parent needs. After writing a subclass, instantiate it and call one inherited method that depends on parent state - if that works, initialisation is wired correctly.

Mistake 7

Debugs by guessing: wraps a failing block in `try: ... except Exception: pass`, or scatters print statements, without ever reading the last line of the traceback or the exact error message and line number.

The red error output feels like noise, and the candidate has not built the habit of reading it bottom-up: exception type and message on the last line, then the file and line where it was raised. Swallowing the exception makes the failure disappear from view but not from the program.

What to do instead

Read the traceback from the bottom: the exception type, the message, then the deepest frame's file and line. Reproduce the failure in the smallest possible snippet before changing anything. Never use a bare except: - catch the specific exception type you can actually handle, and let everything else propagate. Add a regression test that fails before the fix and passes after it.

Mistake 8

Reads a text file line by line and compares or parses values without stripping the trailing newline, so `line == "admin"` is false and numeric parsing throws; or reads a file twice in one program and gets an empty second result because the file pointer is already at the end.

readline() and iteration keep the line terminator, and a file handle is a cursor, not a snapshot. Neither behaviour is visible when printing, because the extra \n just moves the cursor to the next line.

What to do instead

Strip on read - line.strip() (or rstrip('\n')) - before comparing or converting, and remember int('12\n') may work while '12\n' == '12' does not. Open files with a context manager (with open(...) as f:) so the handle closes even on error, and if you need the contents twice, store them in a list once or call f.seek(0) deliberately.

Mistake 9

Runs a binary search, or any algorithm that assumes ordering, against data that was never sorted, and treats the wrong answer as a bug in the search code. Relatedly, calls `sorted(items)` expecting the original list to be reordered and then searches the unsorted original.

The precondition of the algorithm is stated in the textbook but not in the candidate's mental model, and the two similarly named operations - in-place list.sort() versus returning sorted() - differ by one character while behaving completely differently.

What to do instead

Before using a log-time search, write down and check the precondition: is this collection actually sorted, and by the same key I am comparing? Distinguish in-place from returning variants explicitly (items.sort() returns None; items = sorted(items) returns a new list). Test the search on the first element, the last element, an absent value, and a one-element list, since off-by-one and empty-input errors concentrate there.

Mistake 10

Commits with `git add .` from the repository root, sweeping build output, local config, and editor files into the commit, or force-pushes / resolves a merge conflict by deleting the marker lines and keeping whichever side looks familiar.

The candidate treats version control as "save my work" rather than as a curated history for other people. Conflict markers look like broken syntax, so the fastest way to make the file parse again is to delete lines rather than to reconcile the two changes.

What to do instead

Run git status and git diff --staged before every commit and confirm each file belongs in it; keep a .gitignore for generated artefacts. Commit small, single-purpose changes with a message that states why. For conflicts, read both sides, understand what each change intended, keep the correct combination, run the tests, and only then git add the resolved file - never commit a file that still contains <<<<<<< markers.