# Python Mastery Lab — 50 lessons

DiscoveryVIP · October 9, 2026

Use def solve(data) as the entry point. The browser runs real Python via a bundled Pyodide runtime. Exercise inputs and outputs use JSON-compatible values; null becomes None, true becomes True and false becomes False.

## 1. Your first Python value

Python executes statements in order. A function groups work under a name; return sends a value to the caller. print displays information but does not return it. This lab calls solve(data) with test inputs and checks its returned value.

Challenge: Return the text Hello, Python!

Hint: Use quotes for text and indent the return line by four spaces.

```python
def solve(data):
    return "Hello, Python!"
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 2. Name a value

Variables bind names to objects. Assignment uses one equals sign. Python does not require a type declaration, but the value still has a type. Meaningful names make it easier to understand a calculation than single-letter placeholders.

Challenge: Assign data to a variable named learner, then return learner.

Hint: Assignment creates the learner binding.

```python
def solve(data):
    learner = data
    return learner
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 3. Calculate with numbers

Addition, subtraction and multiplication produce new numeric values. Parentheses control grouping. Python integers can grow beyond fixed 32-bit limits, although memory is still finite. Floating-point arithmetic has precision limits, so money needs a deliberate representation.

Challenge: Return data multiplied by 3, then increased by 2.

Hint: Multiplication happens before addition.

```python
def solve(data):
    return data * 3 + 2
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 4. Understand division and remainder

The / operator performs true division. // is floor division, and % gives the corresponding remainder. With positive integers, they answer how many full groups fit and how many items remain. Negative operands need extra care because floor means toward negative infinity.

Challenge: Return [full groups of 4, remainder] for nonnegative data.

Hint: Use // and % with the same divisor.

```python
def solve(data):
    return [data // 4, data % 4]
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 5. Format a readable message

An f-string evaluates expressions inside braces. It is convenient for readable messages without manual string concatenation. The f prefix goes before the quote. Formatting does not change the original value.

Challenge: Return Welcome, NAME! with data as the name.

Hint: Put data inside braces in an f-string.

```python
def solve(data):
    return f"Welcome, {data}!"
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 6. Inspect types

type returns the type of an object. Its __name__ gives a short readable name. Booleans are a distinct type even though they are a subclass of int. Knowing a value’s type helps explain which operations are valid.

Challenge: Return type(data).__name__.

Hint: Use type(data), then read __name__.

```python
def solve(data):
    return type(data).__name__
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 7. Convert numeric text

int converts an integer-like string into an integer and raises ValueError for invalid input. Conversion is a boundary where you should decide how to handle bad data. It differs from simply concatenating two strings containing digits.

Challenge: Convert data to an integer and add 5.

Hint: Call int before doing arithmetic.

```python
def solve(data):
    return int(data) + 5
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 8. Compare values

== compares values; = assigns a binding. Comparisons usually produce True or False. Python considers 1 == True to be true, so type-sensitive validation sometimes needs an additional check. Strings compare by their contents.

Challenge: Return whether data["a"] equals data["b"].

Hint: Use ==, not a single equals sign.

```python
def solve(data):
    return data["a"] == data["b"]
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 9. Combine logical conditions

and combines conditions so both must be truthy. or accepts either condition, while not reverses truthiness. These operators short-circuit and can return operands rather than only booleans. Explicit comparisons keep a validation rule clear.

Challenge: Return true if age >=18 and member is True.

Hint: Use and between the two conditions.

```python
def solve(data):
    return data["age"] >= 18 and data["member"] is True
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 10. Handle missing values explicitly

None is Python’s absence marker. Use is None to check it. Empty strings, zero and False are also falsy but may be valid values. Using or as a default replaces all falsy values, which can erase meaningful zeroes.

Challenge: Return 10 for None, otherwise return data unchanged.

Hint: Check identity with None rather than general truthiness.

```python
def solve(data):
    return 10 if data is None else data
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 11. Choose a branch

An if statement selects a block. Python uses indentation to define the block rather than braces. elif adds another condition and else covers the remaining cases. Be precise about inclusive boundaries such as age 18.

Challenge: Return adult if data >=18, otherwise minor.

Hint: End the if line with a colon and indent its body.

```python
def solve(data):
    if data >= 18:
        return "adult"
    return "minor"
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 12. Build a three-way decision

Order matters when conditions overlap. Put the more restrictive condition first or use non-overlapping ranges. A well-chosen final else makes the remaining behavior explicit. Test values exactly on each boundary.

Challenge: Return high for scores >=80, medium for >=50, and low otherwise.

Hint: Check 80 before 50.

```python
def solve(data):
    if data >= 80:
        return "high"
    elif data >= 50:
        return "medium"
    return "low"
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 13. Normalize text

Strings are immutable. strip and lower return new strings rather than editing the original. Normalization is useful for matching but should not destroy a raw record you need to preserve. The exact rule matters when whitespace or case carries meaning.

Challenge: Return data stripped of surrounding whitespace and lowercased.

Hint: Chain strip() and lower().

```python
def solve(data):
    return data.strip().lower()
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 14. Slice a string

A slice selects a range from a sequence. The stop index is excluded and omitted bounds use the sequence edge. Python strings operate on Unicode code points, which can still differ from a user-perceived character made from multiple code points.

Challenge: Return the first three characters of data.

Hint: Use [:3] to stop before index 3.

```python
def solve(data):
    return data[:3]
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 15. Split and join

split separates a string into pieces; join combines strings using a separator. Calling split without an argument treats runs of whitespace as separators. This makes it useful for a simple word normalization task, but not a universal natural-language tokenizer.

Challenge: Split data on whitespace and join the words with hyphens.

Hint: Call join on the separator string.

```python
def solve(data):
    return "-".join(data.split())
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 16. Read a list

A list is an ordered, mutable sequence. Indices start at zero and negative indices count from the end. Reading an unavailable index raises IndexError. Define what an empty collection should mean before accessing its first value.

Challenge: Return the first item, or None if the list is empty.

Hint: Check the list, not the truthiness of its first item.

```python
def solve(data):
    return data[0] if data else None
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 17. Copy before appending

Lists are mutable, and two names can refer to the same list. list.copy creates a shallow copy, so nested objects may still be shared. Avoid changing a caller’s collection when your function is meant to produce a new result.

Challenge: Return a new list with data["item"] appended to data["items"].

Hint: Copy first; append returns None, not the updated list.

```python
def solve(data):
    result = data["items"].copy()
    result.append(data["item"])
    return result
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 18. Transform with a comprehension

A list comprehension describes a new list from an iterable. Keep it simple enough to read as a transformation. Nested conditions or side effects often belong in a normal loop. The source list remains unchanged here.

Challenge: Return a list containing each input number doubled.

Hint: Place the expression before for n in data.

```python
def solve(data):
    return [n * 2 for n in data]
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 19. Filter values

A comprehension can include a condition that selects which values become results. Filtering and transformation are separate ideas even when written in one expression. A boundary test such as >0 excludes zero.

Challenge: Keep only numbers greater than zero.

Hint: Put if n > 0 after the iteration clause.

```python
def solve(data):
    return [n for n in data if n > 0]
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 20. Sort without mutating

sorted returns a new sorted list, while list.sort changes the existing list and returns None. A key function specifies which value to compare. reverse=True changes direction. Keep the original when it represents user order or source evidence.

Challenge: Return data sorted ascending without changing data.

Hint: Use sorted(data), not data.sort().

```python
def solve(data):
    return sorted(data)
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 21. Look up dictionary data

A dictionary maps keys to values. Indexing a missing key raises KeyError, while get can supply a fallback. A fallback for a missing key does not replace an explicitly stored None. Model those cases according to your data contract.

Challenge: Return data["name"], defaulting to Guest if the key is missing.

Hint: Use get with a default argument.

```python
def solve(data):
    return data.get("name", "Guest")
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 22. Update a dictionary copy

Dictionary unpacking copies key-value pairs into a new dictionary. Later entries replace earlier keys. Like a shallow list copy, nested values are still shared. Separate the transformation from any write to an external system.

Challenge: Return a copy of data with done set to True.

Hint: Unpack data first, then overwrite done.

```python
def solve(data):
    return {**data, "done": True}
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 23. Iterate key-value pairs

items provides key-value pairs from a dictionary. Unpacking gives names to both parts of each pair. Current Python dictionaries preserve insertion order, but choose explicit sorting if the display order itself is a requirement.

Challenge: Return keys whose values are exactly True.

Hint: Use items() and a value is True condition.

```python
def solve(data):
    return [key for key, value in data.items() if value is True]
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 24. Use a set for uniqueness

A set removes duplicate hashable values and supports membership tests. A set’s iteration order is not a display-order contract. Sort the result if callers need a predictable order. Lists and dictionaries cannot be set elements because they are mutable and unhashable.

Challenge: Return unique input integers in ascending order.

Hint: Construct a set, then sort it.

```python
def solve(data):
    return sorted(set(data))
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 25. Unpack a pair

Unpacking assigns sequence elements to several names. The number of elements must match the pattern unless a starred target collects the remainder. Tuples are immutable sequences, but their contents can still refer to mutable objects.

Challenge: Unpack the two input numbers and return their product.

Hint: Use width, height = data.

```python
def solve(data):
    width, height = data
    return width * height
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 26. Trace a for loop

A for loop visits values from an iterable. An accumulator records progress across iterations. A loop should have an explicit purpose such as a running total. Use the visualizer to compare the value before and after each update.

Challenge: Sum all numbers in data using a loop.

Hint: Initialize total outside the loop.

```python
def solve(data):
    total = 0
    for number in data:
        total += number
    return total
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 27. Understand range boundaries

range represents an integer sequence without constructing a full list. Its stop value is excluded. To include n in a sequence starting at 1, stop at n+1. Empty ranges are useful boundary cases rather than errors.

Challenge: Return a list of integers from 1 through data. Input is nonnegative.

Hint: The range stop must be one beyond the last included value.

```python
def solve(data):
    return list(range(1, data + 1))
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 28. Use enumerate for positions

enumerate pairs each item with a counter. The counter defaults to zero but can start elsewhere. This is clearer than maintaining a separate index variable when both position and value are needed.

Challenge: Return labels like 1. HTML for each input item.

Hint: Pass start=1 to enumerate.

```python
def solve(data):
    return [f"{i}. {name}" for i, name in enumerate(data, start=1)]
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 29. Combine sequences with zip

zip pairs corresponding elements and stops at the shortest iterable by default. That behavior is useful when expected but can hide missing data. Validate lengths or use strict=True when mismatched inputs must be rejected. This exercise explicitly uses the shortest behavior.

Challenge: Return pairs from data["names"] and data["scores"] as lists.

Hint: Zip the two lists; convert each tuple to a list for the result.

```python
def solve(data):
    return [list(pair) for pair in zip(data["names"], data["scores"])]
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 30. Stop at the first match

break exits a loop; return exits the whole function. Either can express a search that stops when its result is known. Avoid scanning every element after finding the answer. Define a missing-result value explicitly.

Challenge: Return the first negative number, or None.

Hint: Return inside the conditional and use None after the loop.

```python
def solve(data):
    for number in data:
        if number < 0:
            return number
    return None
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 31. Write a reusable helper

Parameters are local names for supplied arguments. A helper function can separate a small calculation from the workflow that uses it. Explicit inputs and return values make the helper easier to test and reuse than hidden global state.

Challenge: Define double(number) inside solve, then return double(data).

Hint: Indent the helper body another level.

```python
def solve(data):
    def double(number):
        return number * 2
    return double(data)
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 32. Use defaults carefully

Default arguments apply when an argument is omitted, not when None is explicitly supplied. Mutable default values are created once at function definition and can accidentally share state. Use immutable defaults or create a fresh mutable value inside the function.

Challenge: Define greet(name="friend") and use data.get("name", "friend") to return Hi NAME.

Hint: Use the fallback only when the key is missing.

```python
def solve(data):
    def greet(name="friend"):
        return f"Hi {name}"
    return greet(data.get("name", "friend"))
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 33. Collect variable arguments

A starred parameter collects positional arguments into a tuple. Starred unpacking in a call expands a sequence into arguments. These use similar syntax in opposite directions. Keep the function contract clear so callers know what is accepted.

Challenge: Define add(*numbers), then return add(*data).

Hint: Use * in the helper definition and at the call site.

```python
def solve(data):
    def add(*numbers):
        return sum(numbers)
    return add(*data)
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 34. Choose a sorting key

A key function supplies the comparison value for each item. A short lambda can be appropriate for a simple field lookup. sorted preserves the order of items with equal keys, which is useful when the input order carries a secondary meaning.

Challenge: Return input records sorted by score ascending.

Hint: Use key=lambda row: row["score"].

```python
def solve(data):
    return sorted(data, key=lambda row: row["score"])
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 35. See a closure retain state

An inner function can retain access to an enclosing function’s bindings. nonlocal lets it reassign an enclosing binding instead of creating a new local one. This can model small private state, but explicit state objects may be clearer in larger systems.

Challenge: Create a counter starting at data; call it twice and return the results.

Hint: Declare nonlocal count inside the nested function.

```python
def solve(data):
    count = data
    def next_count():
        nonlocal count
        count += 1
        return count
    return [next_count(), next_count()]
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 36. Catch an expected exception

try and except let you handle failures at an appropriate boundary. Catch the exception type you expect rather than hiding every error. Conversion is a natural boundary for user-supplied text. Unexpected programming errors should still be visible during development.

Challenge: Convert data to int, returning invalid on ValueError or TypeError.

Hint: Catch the two specific conversion error types.

```python
def solve(data):
    try:
        return int(data)
    except (ValueError, TypeError):
        return "invalid"
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 37. Parse JSON data

json.loads converts JSON text into Python values. Parsing checks syntax but not whether the resulting fields meet your application contract. JSON null becomes None and JSON booleans become Python booleans. Keep text separate from executable code.

Challenge: Parse the JSON string and return its name field.

Hint: Import json and call json.loads.

```python
import json

def solve(data):
    return json.loads(data)["name"]
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 38. Read CSV from a string

CSV has quoting rules that make naive splitting unreliable. csv.DictReader uses the first row as field names and handles quoted commas. StringIO makes text behave like a file in memory. Values from CSV are strings until you convert them.

Challenge: Read CSV text and return the name column from each record.

Hint: Use DictReader on an io.StringIO object.

```python
import csv
import io

def solve(data):
    return [row["name"] for row in csv.DictReader(io.StringIO(data))]
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 39. Work with dates explicitly

date.fromisoformat reads an ISO calendar date such as 2026-10-09. Adding timedelta advances by days using calendar arithmetic. A date has no time zone or time of day. Datetime workflows need explicit decisions about offsets and local times.

Challenge: Add data["days"] to data["date"] and return an ISO date.

Hint: Use timedelta(days=...) rather than manually changing the day number.

```python
from datetime import date, timedelta

def solve(data):
    start = date.fromisoformat(data["date"])
    return (start + timedelta(days=data["days"])).isoformat()
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 40. Count values with Counter

Counter is a standard-library dictionary subclass for frequencies. It makes the intent of a count clearer than repeating a manual update loop. Converting it to dict gives a simple result shape. Counts describe records, not automatically unique people or events.

Challenge: Count how often each label appears and return a dictionary.

Hint: Import Counter from collections.

```python
from collections import Counter

def solve(data):
    return dict(Counter(data))
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 41. Represent a small object

A class groups state and behavior when that is a useful model. __init__ initializes an instance and self refers to that instance. Not every function needs a class. Use an object when the related data and operations make the code clearer.

Challenge: Create a Rectangle class with area(), then return the area from data width and height.

Hint: Methods need self as the first parameter.

```python
def solve(data):
    class Rectangle:
        def __init__(self, width, height):
            self.width = width
            self.height = height
        def area(self):
            return self.width * self.height
    return Rectangle(data["width"], data["height"]).area()
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 42. Describe records with a dataclass

A dataclass generates common methods for a class based on annotated fields. Annotations describe intent but do not automatically validate runtime types. asdict converts an instance into a dictionary, recursively processing nested dataclasses and containers.

Challenge: Define a dataclass Task with title and done=False; return it as a dictionary.

Hint: Use @dataclass above the class and asdict on the instance.

```python
from dataclasses import dataclass, asdict

def solve(data):
    @dataclass
    class Task:
        title: str
        done: bool = False
    return asdict(Task(data))
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 43. Generate values lazily

A generator yields values one at a time. It can process a sequence without constructing every intermediate value at once. A generator is consumed as it is iterated, so do not assume it can be reused like a list. This exercise converts the final sequence for inspection.

Challenge: Use a generator to yield squares from 0 through data-1, then return a list.

Hint: Use yield inside the loop instead of return.

```python
def solve(data):
    def squares(n):
        for i in range(n):
            yield i * i
    return list(squares(data))
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 44. Handle a virtual text file

with manages a resource so cleanup occurs even when a block exits through an exception. StringIO is an in-memory text stream, useful for examples and tests without touching a real file. The browser runtime uses a virtual filesystem, not unrestricted access to your computer.

Challenge: Read data through StringIO and return its nonempty stripped lines.

Hint: Iterate the stream inside with and filter blank lines.

```python
import io

def solve(data):
    with io.StringIO(data) as stream:
        return [line.strip() for line in stream if line.strip()]
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 45. Validate records at a boundary

Good validation distinguishes absent data, wrong types and unacceptable values. bool is a subclass of int, so isinstance(True, int) is true. For this exercise, type(value) is int deliberately rejects booleans. Real schemas should document every accepted input.

Challenge: Return true only if age is an int (not bool) and at least 18.

Hint: Check the exact type before comparing the number.

```python
def solve(data):
    age = data.get("age")
    return type(age) is int and age >= 18
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 46. Project: receipt calculator

A receipt total is a small pipeline with a clear business rule. Only included items contribute. Use integer cents so arithmetic remains exact in these examples. A real checkout also needs tax, discounts and rounding rules that are outside this exercise.

Challenge: Sum price * quantity for items with included True.

Hint: Use a generator expression inside sum.

```python
def solve(data):
    return sum(item["price"] * item["quantity"] for item in data if item["included"])
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 47. Project: searchable records

Define the query normalization and match rule before writing a search. This exercise trims whitespace, ignores case and matches a substring. It is not fuzzy search. Keep the original records in the result so display text is unchanged.

Challenge: Return records whose name contains the trimmed query, ignoring case.

Hint: Normalize the query and each comparison string.

```python
def solve(data):
    query = data["query"].strip().lower()
    return [row for row in data["items"] if query in row["name"].lower()]
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 48. Project: group sales totals

Grouping combines records under a shared key and then aggregates values. Keep the accumulator initialization explicit and avoid replacing earlier totals with the newest value. Explain the units and input coverage when presenting the report.

Challenge: Return totals by category from records containing category and amount.

Hint: Use get(category, 0) before adding.

```python
def solve(data):
    totals = {}
    for row in data:
        category = row["category"]
        totals[category] = totals.get(category, 0) + row["amount"]
    return totals
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 49. Project: task-state update

A state transformation should return the next state without changing the previous one when history matters. Copy each changed dictionary and build a new list. This makes undo, tests and downstream rendering easier to reason about.

Challenge: Toggle done for the task matching data["id"], leaving the input unchanged.

Hint: Copy the matching dictionary and invert its done value.

```python
def solve(data):
    return [{**task, "done": not task["done"]} if task["id"] == data["id"] else task.copy() for task in data["tasks"]]
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.

## 50. Capstone: clean and rank a report

A dependable report has explicit input rules and a repeatable pipeline. This exercise accepts finite int or float scores but excludes booleans and numeric strings. Trim names, exclude blank ones, then sort highest score first. Preserve the source records for audit and correction.

Challenge: Return cleaned name/score records with nonblank names and finite numeric scores, sorted descending.

Hint: Validate, normalize, collect, then sort with reverse=True.

```python
import math

def solve(data):
    cleaned = []
    for row in data:
        score = row["score"]
        name = row["name"].strip()
        if name and type(score) in (int, float) and math.isfinite(score):
            cleaned.append({"name": name, "score": score})
    return sorted(cleaned, key=lambda row: row["score"], reverse=True)
```

Experiment: Try a new input in the experiment panel. Predict the result before running, then explain any difference.
