{
  "assessmentTests": {
    "python_test": {
      "name": "Python Test",
      "desc": "30 scenario questions on syntax, functions, data structures and object behaviour — find out whether your Python matches what a job posting means by Python proficiency.",
      "recommendation": "Your Python skills profile",
      "results": {
        "beginner": {
          "name": "Beginner",
          "desc": "You can follow a short script and change values inside it, but the questions you missed cluster around the language's own specific behaviour rather than general logic — a default argument that is built once and reused, is versus == on two equal-looking lists, a variable that becomes local the moment you assign to it anywhere in a function. None of this is about being bad at programming; these are Python-specific rules that trip up people coming from other languages just as often as beginners. They matter at work because each one is where a bug hides in code that looks correct at a glance.",
          "recommendation": "Start with three things, in this order: mutable default arguments (why basket=[] is dangerous), the difference between is and ==, and how variable scope works inside a function. The official Python tutorial at docs.python.org covers all three directly, and the term \"Python gotchas\" turns up worked examples of each."
        },
        "intermediate": {
          "name": "Intermediate",
          "desc": "You handle everyday scripting comfortably — data structures, control flow, straightforward functions — and would not be slowed down by routine maintenance work or a small feature on an existing codebase. The gap between here and Advanced is mostly in what happens when Python's convenience features interact with each other: a shallow copy leaving nested data shared when you expected it duplicated, a generator's return value quietly not becoming part of its output, decorators stacking in an order that is easy to get backwards. Those are the kind of bugs a senior reviewer flags in ten seconds and everyone else spends an hour chasing.",
          "recommendation": "Focus on object and iteration internals: shallow versus deep copy, what a generator actually does with return, and how decorator stacking order works. Then class basics that most tutorials skip — default equality without __eq__, and why a mutable class attribute is shared across every instance."
        },
        "advanced": {
          "name": "Advanced",
          "desc": "This is the level most job postings mean by \"strong Python.\" You reach for the right tool without hunting for it, you know why a piece of shared state changed rather than only that it did, and you can read someone else's function and predict its output correctly. What separates this band from the top is the defensive side of the work: knowing which of two correct-looking approaches will misbehave once a class attribute, a keyword-only argument, or an exception hierarchy is involved.",
          "recommendation": "Push into the parts that protect a codebase other people also touch: keyword-only arguments as an API-design choice rather than a syntax curiosity, the exception hierarchy (so except clauses catch the right family of error, not just one class), and context managers as the standard way to guarantee cleanup. Real Python's deep-dive articles are a good next stop for all three."
        },
        "expert": {
          "name": "Expert",
          "desc": "You scored at the top of every section — core syntax, functions and scope, data structures, and object-oriented idioms. Practically, that means you can be handed a stranger's file and explain why it produces the output it does, not just what the syntax says it should do, which is the harder and more valuable skill. At this level the raw language is rarely the limiting factor in your work; the limit is usually the size or shape of the system around the code.",
          "recommendation": "The returns are now in tooling and review: static type checking with mypy or pyright, profiling before optimising rather than guessing, and reading other people's pull requests for exactly this category of bug — the ones that pass every test and still do the wrong thing under a specific input. If you are being screened for a role, describe a bug like this that you found rather than naming language features — it demonstrates the reasoning, not just the vocabulary."
        }
      },
      "questions": [
        {
          "question": "You write def add_item(item, basket=[]): basket.append(item); return basket, then call add_item('a') and add_item('b'), both times leaving basket out. What does the second call return?",
          "options": [
            {
              "icon": "",
              "label": "['b'] — each call gets a fresh empty list by default"
            },
            {
              "icon": "",
              "label": "TypeError — you must supply a basket argument"
            },
            {
              "icon": "",
              "label": "['a', 'b'] — the default list is created once and reused across calls"
            },
            {
              "icon": "",
              "label": "['a'] then ['b'] as two independent single-item lists"
            }
          ]
        },
        {
          "question": "a = [1, 2, 3] and b = [1, 2, 3]. What does a == b return, and what does a is b return?",
          "options": [
            {
              "icon": "",
              "label": "True for both — identical lists are always the same object"
            },
            {
              "icon": "",
              "label": "True for ==, False for is — equal contents but different objects in memory"
            },
            {
              "icon": "",
              "label": "False for both — separate literals are never equal in Python"
            },
            {
              "icon": "",
              "label": "True for is, False for == — is checks value equality for lists"
            }
          ]
        },
        {
          "question": "In Python 3, what does 7 / 2 evaluate to, and what does 7 // 2 evaluate to?",
          "options": [
            {
              "icon": "",
              "label": "3.5, then 3 — / always returns a float, // floors the result"
            },
            {
              "icon": "",
              "label": "3, then 3.5 — / truncates and // returns a float"
            },
            {
              "icon": "",
              "label": "3.5, then 3.5 — both operators return a float in Python 3"
            },
            {
              "icon": "",
              "label": "3, then 3 — both operators floor the result in Python 3"
            }
          ]
        },
        {
          "question": "s = 'hello', then you run s[0] = 'H'. What happens?",
          "options": [
            {
              "icon": "",
              "label": "s becomes 'Hello'"
            },
            {
              "icon": "",
              "label": "s becomes 'Hhello' because the character is inserted, not replaced"
            },
            {
              "icon": "",
              "label": "TypeError — strings are immutable and don't support item assignment"
            },
            {
              "icon": "",
              "label": "Nothing — the assignment is silently ignored and s stays 'hello'"
            }
          ]
        },
        {
          "question": "x = [10, 20, 30, 40, 50]. What does x[1:4] return?",
          "options": [
            {
              "icon": "",
              "label": "[20, 30, 40, 50]"
            },
            {
              "icon": "",
              "label": "[10, 20, 30, 40]"
            },
            {
              "icon": "",
              "label": "[20, 30, 40]"
            },
            {
              "icon": "",
              "label": "[30, 40]"
            }
          ]
        },
        {
          "question": "name = 'Ana' and age = 30. What does f'{name} is {age}' produce?",
          "options": [
            {
              "icon": "",
              "label": "{name} is {age}"
            },
            {
              "icon": "",
              "label": "Ana is 30"
            },
            {
              "icon": "",
              "label": "name is age"
            },
            {
              "icon": "",
              "label": "A TypeError, because f-strings require the format() method"
            }
          ]
        },
        {
          "question": "What does 1 < 2 < 3 evaluate to?",
          "options": [
            {
              "icon": "",
              "label": "True — Python chains the comparisons as (1 < 2) and (2 < 3)"
            },
            {
              "icon": "",
              "label": "False — chained comparisons are evaluated left to right without short-circuiting, giving 1 < 2 < 3 -> 1 < True -> False"
            },
            {
              "icon": "",
              "label": "A SyntaxError — comparisons cannot be chained"
            },
            {
              "icon": "",
              "label": "3 — Python returns the last value in the chain"
            }
          ]
        },
        {
          "question": "What does bool([]) return, and what does bool([0]) return?",
          "options": [
            {
              "icon": "",
              "label": "False, then False — any list containing only falsy values is falsy"
            },
            {
              "icon": "",
              "label": "False, then True — an empty list is falsy, a list containing one element (even 0) is truthy"
            },
            {
              "icon": "",
              "label": "True, then True — non-None objects are always truthy"
            },
            {
              "icon": "",
              "label": "True, then False — a list is truthy only when empty"
            }
          ]
        },
        {
          "question": "A module-level count = 0 exists. Inside def increment(): count += 1; return count, you call increment(). What happens?",
          "options": [
            {
              "icon": "",
              "label": "UnboundLocalError — assigning to count anywhere in the function makes it local, so count += 1 tries to read a local variable before it is assigned"
            },
            {
              "icon": "",
              "label": "It returns 1 and increments the module-level count to 1"
            },
            {
              "icon": "",
              "label": "It returns 1 but leaves the module-level count at 0"
            },
            {
              "icon": "",
              "label": "It raises NameError because count was never declared inside the function"
            }
          ]
        },
        {
          "question": "def f(*args, **kwargs): return args, kwargs. What does f(1, 2, x=3) return?",
          "options": [
            {
              "icon": "",
              "label": "([1, 2], {'x': 3})"
            },
            {
              "icon": "",
              "label": "((1, 2), {'x': 3})"
            },
            {
              "icon": "",
              "label": "(1, 2, {'x': 3})"
            },
            {
              "icon": "",
              "label": "TypeError — positional and keyword arguments cannot be mixed in one call"
            }
          ]
        },
        {
          "question": "You build funcs = [lambda: i for i in range(3)] in a loop. What does [f() for f in funcs] return?",
          "options": [
            {
              "icon": "",
              "label": "[2, 2, 2] — each lambda looks up i when called, and by then the loop has finished with i equal to 2"
            },
            {
              "icon": "",
              "label": "[0, 1, 2] — each lambda captures the value of i at the time it was created"
            },
            {
              "icon": "",
              "label": "[0, 0, 0] — lambdas always capture the first value assigned to a variable"
            },
            {
              "icon": "",
              "label": "A NameError, because i no longer exists once the loop ends"
            }
          ]
        },
        {
          "question": "A function is defined with @a directly above @b, which sits directly above def f(): pass. In what order are the decorators applied to f?",
          "options": [
            {
              "icon": "",
              "label": "b wraps f first, then a wraps the result of b — the decorator closest to the function runs first"
            },
            {
              "icon": "",
              "label": "a wraps f first, then b wraps the result — decorators apply top to bottom"
            },
            {
              "icon": "",
              "label": "Both decorators wrap f independently and only the last one defined takes effect"
            },
            {
              "icon": "",
              "label": "It depends on the order the decorators are imported, not their position in the code"
            }
          ]
        },
        {
          "question": "def f(a, *, b): return a + b. Which of these calls works?",
          "options": [
            {
              "icon": "",
              "label": "f(1, b=2) — b must be passed by keyword because it comes after the bare * in the signature"
            },
            {
              "icon": "",
              "label": "f(1, 2) — positional arguments fill parameters left to right regardless of the *"
            },
            {
              "icon": "",
              "label": "f(a=1, 2) — keyword arguments must come first"
            },
            {
              "icon": "",
              "label": "f(1) — b is optional because it follows the *"
            }
          ]
        },
        {
          "question": "A generator function yields 1, then yields 2, then does return 99. What does list(the_generator()) contain, and what happens to the 99?",
          "options": [
            {
              "icon": "",
              "label": "[1, 2, 99] — return inside a generator behaves like one more yield"
            },
            {
              "icon": "",
              "label": "[99] — return immediately overrides any values already yielded"
            },
            {
              "icon": "",
              "label": "[1, 2] — the return value of a generator becomes the value attached to the StopIteration it raises, it is not yielded"
            },
            {
              "icon": "",
              "label": "A RuntimeError, because generators cannot contain a return statement"
            }
          ]
        },
        {
          "question": "In an outer() function, x = 1 is set, then a nested inner() does nonlocal x; x += 1, and outer() calls inner() before returning x. What does outer() return?",
          "options": [
            {
              "icon": "",
              "label": "1 — inner() operates on its own local copy of x, so the change does not persist"
            },
            {
              "icon": "",
              "label": "An UnboundLocalError, because x is not declared inside inner() before the nonlocal statement"
            },
            {
              "icon": "",
              "label": "A SyntaxError — nonlocal can only be used inside classes"
            },
            {
              "icon": "",
              "label": "2 — nonlocal lets inner() modify the outer function's x instead of creating a new local variable"
            }
          ]
        },
        {
          "question": "d = {'a': 1, 'b': 2}. What does d.get('c', 0) return, and does it add 'c' to the dictionary?",
          "options": [
            {
              "icon": "",
              "label": "0, and no — get() returns the default without inserting the key"
            },
            {
              "icon": "",
              "label": "0, and yes — get() inserts the key with the default value"
            },
            {
              "icon": "",
              "label": "KeyError, because 'c' does not exist"
            },
            {
              "icon": "",
              "label": "None, and yes — get() always adds missing keys with a None value"
            }
          ]
        },
        {
          "question": "t = (1, 2, [3, 4]). Can you run t[2].append(5), and can you run t[0] = 10?",
          "options": [
            {
              "icon": "",
              "label": "Both work — nesting a list inside a tuple makes the whole tuple mutable"
            },
            {
              "icon": "",
              "label": "Both raise TypeError — nothing inside a tuple can ever be changed"
            },
            {
              "icon": "",
              "label": "t[2].append(5) raises TypeError, but t[0] = 10 works because tuples allow reassignment of their first element"
            },
            {
              "icon": "",
              "label": "t[2].append(5) works because the list inside is mutable; t[0] = 10 raises TypeError because the tuple itself is immutable"
            }
          ]
        },
        {
          "question": "Which of these returns an iterator that computes values lazily rather than building the whole sequence in memory at once: [x*x for x in range(1000000)] or (x*x for x in range(1000000))?",
          "options": [
            {
              "icon": "",
              "label": "The one with parentheses, (x*x for x in range(1000000)) — that's a generator expression"
            },
            {
              "icon": "",
              "label": "The one with square brackets — list comprehensions are always lazy in Python 3"
            },
            {
              "icon": "",
              "label": "Both are lazy — the only difference is the type returned at the end"
            },
            {
              "icon": "",
              "label": "Neither — both build the full sequence in memory immediately"
            }
          ]
        },
        {
          "question": "A try block divides by zero, the except ZeroDivisionError clause prints 'a', and a finally clause prints 'b'. What prints, and in what order?",
          "options": [
            {
              "icon": "",
              "label": "b then a — finally always runs before any except clause"
            },
            {
              "icon": "",
              "label": "a then b — the matching except runs first, and finally always runs afterward regardless"
            },
            {
              "icon": "",
              "label": "Only b — the exception is not actually caught, so 'a' never prints"
            },
            {
              "icon": "",
              "label": "Only a — finally is skipped once an exception has already been handled"
            }
          ]
        },
        {
          "question": "import copy; a = [[1, 2], [3, 4]]; b = copy.copy(a); b[0].append(99). Does a[0] change too?",
          "options": [
            {
              "icon": "",
              "label": "No — copy.copy() always duplicates nested objects as well"
            },
            {
              "icon": "",
              "label": "No — appending to b[0] creates a brand-new list for b[0] only"
            },
            {
              "icon": "",
              "label": "Yes, but only because copy.copy() was called without the deepcopy argument"
            },
            {
              "icon": "",
              "label": "Yes — copy.copy() makes a shallow copy, so the nested lists are still shared between a and b"
            }
          ]
        },
        {
          "question": "d = {'x': 1, 'y': 2}. What does 'x' in d check against — the keys, the values, or both?",
          "options": [
            {
              "icon": "",
              "label": "The values — `in` checks whether 1 or 2 equals 'x'"
            },
            {
              "icon": "",
              "label": "Both keys and values are checked"
            },
            {
              "icon": "",
              "label": "Neither — `in` on a dict always raises TypeError"
            },
            {
              "icon": "",
              "label": "The keys — `in` on a dictionary tests membership among its keys, not its values"
            }
          ]
        },
        {
          "question": "names = ['Al', 'Bo'] and scores = [10, 20]. What does list(zip(names, scores)) return?",
          "options": [
            {
              "icon": "",
              "label": "[('Al', 'Bo'), (10, 20)]"
            },
            {
              "icon": "",
              "label": "['Al', 10, 'Bo', 20]"
            },
            {
              "icon": "",
              "label": "[('Al', 10), ('Bo', 20)]"
            },
            {
              "icon": "",
              "label": "A ValueError, because zip requires three or more arguments"
            }
          ]
        },
        {
          "question": "class Dog: tricks = [] holds a class-level list, and add_trick appends to self.tricks. d1 = Dog() and d2 = Dog() are both created, then d1.add_trick('sit') runs. What does d2.tricks contain?",
          "options": [
            {
              "icon": "",
              "label": "[] — each instance gets its own independent copy of tricks"
            },
            {
              "icon": "",
              "label": "['sit'] — tricks is a class-level list shared by every instance, so d1's change is visible on d2 as well"
            },
            {
              "icon": "",
              "label": "A TypeError — instances cannot append to a class attribute"
            },
            {
              "icon": "",
              "label": "['sit'] only after d2 = Dog() is re-run"
            }
          ]
        },
        {
          "question": "class A has def greet(self): return 'A'. class B(A) has def greet(self): return 'B' + super().greet(). What does B().greet() return?",
          "options": [
            {
              "icon": "",
              "label": "'AB' — super() always runs before the subclass's own code"
            },
            {
              "icon": "",
              "label": "'B' — super().greet() is ignored because B already overrides greet"
            },
            {
              "icon": "",
              "label": "'BA' — super().greet() calls A's method and its result is appended to 'B'"
            },
            {
              "icon": "",
              "label": "An AttributeError, because A has no greet method visible to B"
            }
          ]
        },
        {
          "question": "class Point stores self.x in __init__ and defines no other methods. p1 = Point(1) and p2 = Point(1). What does p1 == p2 return by default?",
          "options": [
            {
              "icon": "",
              "label": "True — Python compares all matching attributes by default"
            },
            {
              "icon": "",
              "label": "False — without a custom __eq__, == falls back to identity comparison, and p1 and p2 are different objects"
            },
            {
              "icon": "",
              "label": "True, but only because both objects were created with the same argument"
            },
            {
              "icon": "",
              "label": "A TypeError, because Point does not define __eq__"
            }
          ]
        },
        {
          "question": "Which except clause below will also catch a ZeroDivisionError: except ArithmeticError, except ValueError, or except KeyError?",
          "options": [
            {
              "icon": "",
              "label": "except ValueError — division errors are treated as invalid values"
            },
            {
              "icon": "",
              "label": "except KeyError — any runtime error not tied to a specific type falls back to KeyError"
            },
            {
              "icon": "",
              "label": "None of them — only except ZeroDivisionError itself can catch it"
            },
            {
              "icon": "",
              "label": "except ArithmeticError — ZeroDivisionError is a subclass of ArithmeticError"
            }
          ]
        },
        {
          "question": "You open a file with open('data.txt') as f: and an exception is raised while reading inside the block. Does the file still get closed?",
          "options": [
            {
              "icon": "",
              "label": "No — the file stays open because the block did not finish normally"
            },
            {
              "icon": "",
              "label": "Only if you also add a finally clause around the with statement"
            },
            {
              "icon": "",
              "label": "Yes — the with statement calls the file's __exit__ method, which closes it, even if an exception occurs inside the block"
            },
            {
              "icon": "",
              "label": "It depends on the exception type — only exceptions caught elsewhere trigger cleanup"
            }
          ]
        },
        {
          "question": "x = [3, 1, 2] and y = x.sort(). What does y hold?",
          "options": [
            {
              "icon": "",
              "label": "[1, 2, 3] — sort() returns the newly sorted list"
            },
            {
              "icon": "",
              "label": "None — list.sort() sorts in place and returns None; x itself becomes [1, 2, 3]"
            },
            {
              "icon": "",
              "label": "[3, 1, 2] — sort() returns the original unsorted list and only changes x's internal order"
            },
            {
              "icon": "",
              "label": "A TypeError, because sort() cannot be assigned to a variable"
            }
          ]
        },
        {
          "question": "What happens when you run 'Score: ' + 42?",
          "options": [
            {
              "icon": "",
              "label": "'Score: 42' — Python converts the number automatically inside a + expression"
            },
            {
              "icon": "",
              "label": "'Score: ' — the number is silently dropped"
            },
            {
              "icon": "",
              "label": "42 — the string is coerced to a number and added"
            },
            {
              "icon": "",
              "label": "TypeError — you cannot concatenate a str and an int directly, 42 must be converted with str() first"
            }
          ]
        },
        {
          "question": "You need to check whether a list called results is empty. Which is the idiomatic Python way: if results == []: , if len(results) == 0: , or if not results: ?",
          "options": [
            {
              "icon": "",
              "label": "if results == []: — this is the only reliable way to detect an empty list"
            },
            {
              "icon": "",
              "label": "if len(results) == 0: — this is required because empty lists are truthy in Python"
            },
            {
              "icon": "",
              "label": "All three are equally never used in practice; the standard approach is if results is None:"
            },
            {
              "icon": "",
              "label": "if not results: — Python idiom relies on truthiness, and an empty list is falsy"
            }
          ]
        }
      ],
      "optionOrderVersion": "92945565db6c55da"
    }
  }
}
