Python Fundamentals for Automation

Build a mini test runner script that outputs a formatted pass/fail report.

Introduction

30 Second Summary

Every testing framework you will ever use follows the same pattern: store what to check, run the checks, and report the results. Understanding this pattern in plain Python makes every automation tool you touch afterwards feel familiar.

In this project, you will build a mini test runner script that stores test scenarios as data, runs each one through check functions, and prints a formatted pass/fail report to your terminal.

What You'll Build

A Python script called test_runner.py that processes test scenarios through reusable check functions and outputs a formatted test report.

By the end of this project, you'll have:

  • A working Python script that runs from the command line and prints a test report.
  • Reusable check functions with parameters and return values that mirror real assertion logic.
  • A mental model of Python's building blocks (variables, functions, conditionals, loops, data structures) ready to apply to real automation tests.
  • Secret Mission: Add a timestamp, a "contains" check type, and a summary counter to your report.

Are there any prerequisites?

You need Python and a code editor (like VS Code or Cursor) installed on your machine. No prior Python experience is required.

Set Up Your Project

Before writing any Python logic, you need a dedicated folder to keep your project files organized and a file to write code in.

In this step, you'll create a project folder on your Desktop, open it in your code editor, and confirm that Python runs correctly.

In this step, get ready to:
  • Create a project folder on your Desktop.
  • Create your first Python file.
  • Verify Python runs correctly in your terminal.
Create your project folder
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir python-test-runner
cd python-test-runner
  • Confirm the folder was created by running:
ls

The output should be empty since the folder is brand new. If you see no error, you're in the right place.

Seeing 'no such file or directory'?

Make sure you ran cd ~/Desktop first. If your Desktop path is different (some systems use a localized name), open your file manager and check the exact folder name.

Help me fix my terminal path

Now open the folder in your code editor so you can create files inside it.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type the name of your code editor (e.g. VS Code or Cursor) and press Enter to open it.
  • In your editor, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the python-test-runner folder, then click Open.

You should see the folder name python-test-runner in the top of the Explorer sidebar on the left.

Create your Python file and add a test line

Your folder is empty. You need a Python file to write code in.

  • In the Explorer panel on the left, hover over the folder name and click the New File icon (it looks like a page with a + sign).
  • Type test_runner.py and press Enter.

The file opens automatically in the editor. Now add a single line of code to confirm Python can run this file.

  • Add this line to test_runner.py:
print("Python is working!")

What does this line do?

The print() function sends text to your terminal. Whatever you put inside the parentheses (in quotes) appears as output when the file runs.

  • Save test_runner.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Run the file and confirm the output

Now you'll tell Python to execute your file and check that it prints the message to the terminal.

  • Switch back to your terminal. Make sure you are still inside the python-test-runner folder.
  • Run your file by entering this command:
python test_runner.py

You should see Python is working! printed in your terminal.

Don't see the output?

  • If you see a 'command not found' error, try python3 test_runner.py instead. Some systems use python3 as the command name.
  • If you see 'No such file or directory', make sure your terminal is inside the python-test-runner folder. Run ls and check that test_runner.py appears in the listing.
  • If you see a syntax error, double-check that your file contains exactly print("Python is working!") with matching quotes and parentheses.

Help me troubleshoot running my Python file

✔️ Awesome, I've got everything!

Great. Double check you've saved your file, then move on to the next step.

ⓧ I'd like to double check the full code

Here is the complete test_runner.py file at this point:

print("Python is working!")

Your environment is ready. Next up, you'll replace this test line with real variables and formatted output.

Store Test Data with Variables and Strings

Your Python environment is working. Now it's time to store the data your test runner will check.

In real automation, tests compare actual values against expected ones. You'll use variables to hold that data and f-strings to display it in a formatted report.

In this step, get ready to:
  • Store test information in variables.
  • Display a formatted report header using f-strings.
Create variables to store test data

Variables are named containers that hold values. In a real test, you would store things like the URL to visit, the page title to expect, and the name of the app under test.

  • In test_runner.py, select all the existing code and delete it.
  • Replace it with the following variables:
# --- Variables and Strings ---
base_url = "https://example.com"
app_name = "TodoMVC"
expected_title = "TodoMVC"

What does this code do?

Each line creates a variable by giving a name, an equals sign, and a value in quotes. The value in quotes is called a string.

  • base_url holds the website address your test would visit.
  • app_name holds the name of the application under test.
  • expected_title holds the page title you expect to see when the test runs.
  • Save test_runner.py.
  • You won't see any output yet. The next chunk adds print statements that display these values.
Print a formatted header with f-strings

Storing data is only useful if you can display it. F-strings let you embed variable values directly inside text by wrapping the variable name in curly braces.

  • Add the following print statements below your variables in test_runner.py:
print("=" * 40)
print(f"TEST REPORT - {app_name}")
print(f"URL: {base_url}")
print("=" * 40)

What does this code do?

  • "=" * 40 repeats the equals sign 40 times to create a visual divider line.
  • The f before the opening quote marks the string as an f-string. Anything inside {} gets replaced with that variable's value.
  • {app_name} becomes TodoMVC and {base_url} becomes https://example.com when the code runs.
  • Save test_runner.py.
  • Run the file by entering this command in your terminal:
python test_runner.py

You should see a formatted header with a line of equals signs, the app name TodoMVC, and the URL https://example.com printed between divider lines.

Not seeing the formatted output?

  • Make sure you deleted the old print("Python is working!") line. Your file should only contain the new variables and print statements.
  • Check that each f-string has the letter f immediately before the opening quote. Without it, Python prints the literal text {app_name} instead of the value.
  • Make sure you saved the file before running the command.

Why isn't my f-string showing variable values?

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete test_runner.py file at this point:

# --- Variables and Strings ---
base_url = "https://example.com"
app_name = "TodoMVC"
expected_title = "TodoMVC"

print("=" * 40)
print(f"TEST REPORT - {app_name}")
print(f"URL: {base_url}")
print("=" * 40)

Your test data is stored and displaying cleanly. Next up, you'll create reusable functions that check whether values match expectations.

Create Reusable Check Functions

Your variables and f-strings from the last step proved that Python can store and display test data. Now you need a way to actually check that data against expected values.

Functions let you package a piece of logic into a reusable block that you can call over and over with different inputs. In this step, you'll create two check functions that mirror the assertions you'd find inside a real testing framework.

In this step, get ready to:
  • Create a check_title function that compares two values.
  • Create a check_element_exists function that searches a list.
  • Test both functions with sample data and verify the output.
Create the check_title function

A function groups related logic behind a name you can call. Your first function will compare an actual page title to the expected one and report whether they match.

  • In test_runner.py, find the four print() lines at the bottom of your file:
print("=" * 40)
print(f"TEST REPORT - {app_name}")
print(f"URL: {base_url}")
print("=" * 40)
  • Delete all four of these lines. You'll replace them with reusable functions.
  • Below the expected_title variable, add this function:
# --- Functions ---
def check_title(actual, expected):
    if actual == expected:
        return "PASS"
    else:
        return "FAIL"

What does this code do?

This function takes two inputs (called parameters) and compares them.

  • def defines a new function. Everything indented below it belongs to that function.
  • actual and expected are parameters. They receive whatever values you pass in when you call the function.
  • return sends a value back to whoever called the function. The function exits immediately when it hits a return statement.

You won't see any output yet. The function is defined but not called. The next substep adds a second function, and then you'll test them both.

Create the check_element_exists function

Your second function checks whether a specific element exists inside a list. This mirrors a real test that verifies a button or link is present on a page.

  • Below the check_title function, add this second function:
def check_element_exists(element_list, target):
    if target in element_list:
        return "PASS"
    else:
        return "FAIL"

What does this code do?

  • element_list is a parameter that expects a list (like a list of page elements).
  • target is the element you're looking for.
  • The in keyword checks if target exists anywhere inside element_list. Python handles the searching for you.

You won't see output yet. The next substep calls both functions so you can verify they work.

Test both functions with sample data

A function does nothing until you call it. Calling a function means writing its name followed by parentheses containing the values you want to pass in.

  • Below the check_element_exists function, add these two lines to call both functions and print their results:
print(check_title("TodoMVC", "TodoMVC"))
print(check_element_exists(["header", "input", "submit"], "login"))

What does this code do?

  • check_title("TodoMVC", "TodoMVC") calls your first function with two matching strings. Since they're equal, it returns "PASS".
  • check_element_exists(["header", "input", "submit"], "login") calls your second function. The list does not contain "login", so it returns "FAIL".
  • Save test_runner.py.
  • Run your file by entering this command in your terminal:
python test_runner.py
  • You should see two lines of output: PASS on the first line and FAIL on the second.

Seeing a NameError or no output?

  • If you see NameError: name 'check_title' is not defined, make sure your print() calls are below (not inside) the function definitions. They should be at the very bottom of the file with no indentation.
  • If you see IndentationError, check that the lines inside each function are indented with exactly 4 spaces. Mixing tabs and spaces causes this error.
  • If you still see the old header output, you may not have deleted the original four print() lines from Step 2.

Why aren't my functions printing PASS and FAIL?

✔️ Awesome, I've got everything!

Great. Double check you've saved your file, and let's keep moving.

ⓧ I'd like to double check the full code

Here's what your complete test_runner.py should look like at this point:

# --- Variables and Strings ---
base_url = "https://example.com"
app_name = "TodoMVC"
expected_title = "TodoMVC"


# --- Functions ---
def check_title(actual, expected):
    if actual == expected:
        return "PASS"
    else:
        return "FAIL"


def check_element_exists(element_list, target):
    if target in element_list:
        return "PASS"
    else:
        return "FAIL"


print(check_title("TodoMVC", "TodoMVC"))
print(check_element_exists(["header", "input", "submit"], "login"))

Why build custom check functions?

Real testing frameworks like pytest work the same way. They provide assertion functions that compare values and report pass or fail. By writing your own, you understand exactly what happens inside those frameworks.

You now have two reusable check functions that can compare values and search lists. Next up, you'll build a single function that decides which check to run based on the type of test it receives.

Route Logic with Conditionals

You now have two separate check functions, but a real test runner needs a single entry point. It needs to look at each test, figure out what kind of check is required, and call the right function automatically.

In this step, you'll build a run_test function that uses Python's if/elif/else conditionals to route each test to the correct check function based on its type.

In this step, get ready to:
  • Create a run_test function that routes tests using conditionals.
  • Test the function with sample data and verify the output.
Create the run_test function

Right now, you call each check function directly. A real test framework reads test data and decides which check to run. Your run_test function will do exactly that using conditional logic.

  • In test_runner.py, find these two lines at the bottom of your file:
print(check_title("TodoMVC", "TodoMVC"))
print(check_element_exists(["header", "input", "submit"], "login"))
  • Delete both of those lines. You'll replace them with the new function and a better test call.
  • Add the following run_test function below check_element_exists:
def run_test(test):
    name = test["name"]
    check_type = test["check_type"]

    if check_type == "title":
        result = check_title(test["actual"], test["expected"])
    elif check_type == "element":
        result = check_element_exists(test["elements"], test["target"])
    else:
        result = "SKIP"

    return {"name": name, "result": result}

What does this code do?

The run_test function accepts a single dictionary called test. It pulls out the test's name and check type, then uses conditionals to decide what happens next.

  • if check_type == "title" routes the test to your check_title function.
  • elif check_type == "element" routes it to check_element_exists instead.
  • else catches anything unexpected and returns "SKIP" so the script never crashes on an unknown type.
  • return {"name": name, "result": result} packages the test name and its pass/fail outcome into a new dictionary. This structured return value is how real test frameworks build reports.
Test the function with sample data

To confirm your routing logic works, you'll pass a sample test dictionary into run_test and print what comes back.

  • Add this line at the very bottom of test_runner.py, below the run_test function:
print(run_test({"name": "Sample test", "check_type": "title", "actual": "TodoMVC", "expected": "TodoMVC"}))

What's happening here?

You're passing a dictionary with a check_type of "title". The if branch will match, call check_title, and return a result dictionary with "PASS" because the actual and expected titles match.

  • Save test_runner.py.
  • Run your file by entering this command in the terminal:
python test_runner.py
  • You should see a dictionary printed: {'name': 'Sample test', 'result': 'PASS'}.

Seeing a KeyError or TypeError?

  • A KeyError means the dictionary you passed is missing a key that run_test expects. Double check your print statement includes all four keys: name, check_type, actual, and expected.
  • A TypeError usually means you forgot the parentheses around the dictionary or have a typo in a string. Make sure each key-value pair uses a colon and the whole dictionary is wrapped in {}.
  • Make sure you deleted the old print lines from the previous step. Having them remain won't cause an error, but the output will be confusing.

Still stuck? Help me debug my run_test function

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here's what your complete test_runner.py should look like at this point:

# --- Variables and Strings ---
base_url = "https://example.com"
app_name = "TodoMVC"
expected_title = "TodoMVC"


# --- Functions ---
def check_title(actual, expected):
    if actual == expected:
        return "PASS"
    else:
        return "FAIL"


def check_element_exists(element_list, target):
    if target in element_list:
        return "PASS"
    else:
        return "FAIL"


def run_test(test):
    name = test["name"]
    check_type = test["check_type"]

    if check_type == "title":
        result = check_title(test["actual"], test["expected"])
    elif check_type == "element":
        result = check_element_exists(test["elements"], test["target"])
    else:
        result = "SKIP"

    return {"name": name, "result": result}


print(run_test({"name": "Sample test", "check_type": "title", "actual": "TodoMVC", "expected": "TodoMVC"}))

Your test runner can now accept a dictionary, read its type, and route it to the right check function. Next up, you'll feed it a whole list of test scenarios and print a formatted report.

Process Test Scenarios with Loops

Your run_test function can route any test dictionary to the right check function and return a result. The missing piece is feeding it multiple tests at once and producing a readable report.

In this step, you will build the data layer (a list of test dictionaries) and the execution layer (loops that process each test and print results). This is the pattern every real test framework uses: data in, loop through, report out.

In this step, get ready to:
  • Create a list of test scenario dictionaries.
  • Loop through scenarios and collect results.
  • Print a formatted test report with pass/fail counts.
Create the test scenarios list

A list in Python is an ordered collection wrapped in square brackets. A dictionary is a set of key-value pairs wrapped in curly braces. Together, they let you describe structured test data that your loop can process one item at a time.

  • In test_runner.py, find the last line of your file (the print(run_test(...)) test call) and delete it.
  • In its place, add the test scenarios list by copying the following code below the run_test function:
# --- Test Scenarios (Data Structures) ---
test_scenarios = [
    {
        "name": "Page title matches expected",
        "check_type": "title",
        "actual": "TodoMVC",
        "expected": "TodoMVC",
    },
    {
        "name": "Page title does not match",
        "check_type": "title",
        "actual": "Wrong Title",
        "expected": "TodoMVC",
    },
    {
        "name": "Submit button exists on page",
        "check_type": "element",
        "elements": ["header", "input", "submit", "footer"],
        "target": "submit",
    },
    {
        "name": "Login link exists on page",
        "check_type": "element",
        "elements": ["header", "input", "submit", "footer"],
        "target": "login",
    },
]

What does this code do?

test_scenarios is a list containing four dictionaries. Each dictionary describes one test with a name, a check_type, and the data that check function needs.

Two of the tests are designed to pass (matching title, existing element). Two are designed to fail (wrong title, missing element). This mirrors how real test suites contain a mix of passing and failing cases.

You won't see a change yet when you run the file. The next chunk uses this data.

Loop through scenarios and collect results

A for loop walks through a list one item at a time and runs the same block of code on each item. You will use one to run every test scenario through run_test and collect the results.

  • Add the following code below the test_scenarios list:
# --- Loop Through Tests ---
results = []

for test in test_scenarios:
    result = run_test(test)
    results.append(result)

What does this code do?

  • results = [] creates an empty list to hold the output from each test run.
  • for test in test_scenarios: grabs one dictionary at a time from the list and assigns it to the variable test.
  • .append(result) adds the returned dictionary to the end of results. After the loop finishes, results holds four dictionaries.

You won't see output yet. The next chunk prints the collected results as a formatted report.

Print the formatted test report

Now that your results are collected, you will loop through them a second time to print a human-readable report. This separation (collect first, display later) keeps your logic clean and reusable.

  • Add the report header and results loop below the code you just wrote:
# --- Print Report ---
print("=" * 40)
print(f"TEST REPORT - {app_name}")
print(f"URL: {base_url}")
print("=" * 40)

for result in results:
    print(f"  [{result['result']}] {result['name']}")

print("=" * 40)

What does this code do?

  • "=" * 40 repeats the equals sign 40 times to create a visual divider line.
  • The f-strings pull in app_name and base_url from the variables you defined at the top of the file.
  • The second for loop walks through results and prints each test name with its PASS or FAIL tag.
  • Save test_runner.py and run it by executing this command:
python test_runner.py
  • You should see a report header with the app name and URL, followed by four test results (two PASS, two FAIL), separated by divider lines.

Seeing a syntax error or no output?

  • Check that you deleted the old print(run_test(...)) line from the previous step. Leftover code can cause unexpected output.
  • Make sure the f-string uses curly braces around the variable names: {app_name} not just app_name.
  • Verify that the indentation inside your for loops uses exactly 4 spaces. Mixed tabs and spaces will cause an indentation error.
  • Why isn't my test report printing?

The report looks good, but you cannot tell at a glance how many passed or failed. Next, you will add counters.

  • Add the pass/fail counter code directly below the last print("=" * 40) line:
pass_count = 0
fail_count = 0

for result in results:
    if result["result"] == "PASS":
        pass_count = pass_count + 1
    else:
        fail_count = fail_count + 1

print(f"  {pass_count} passed, {fail_count} failed")
print("=" * 40)

What does this code do?

  • pass_count and fail_count start at zero. The loop increments one or the other depending on each test's result.
  • This is the same counting pattern real test frameworks use to generate summary statistics.
  • Save test_runner.py and run it again:
python test_runner.py
  • You should now see the full report ending with a summary line: 2 passed, 2 failed.

Counts showing 0 passed or 0 failed?

  • Make sure the counter code is placed after the results-printing loop, not inside it. The indentation level of pass_count = 0 should be at the top level (no indentation), not nested under the previous for loop.
  • Check that your comparison uses == (double equals) and compares against the string "PASS" in uppercase.
  • Why are my pass/fail counts wrong?

✔️ Awesome, I've got everything!

Great. Double check you have saved your file.

ⓧ I'd like to double check the full code

Here is the complete test_runner.py file at this point. Compare yours line by line:

# --- Variables and Strings ---
base_url = "https://example.com"
app_name = "TodoMVC"
expected_title = "TodoMVC"


# --- Functions ---
def check_title(actual, expected):
    if actual == expected:
        return "PASS"
    else:
        return "FAIL"


def check_element_exists(element_list, target):
    if target in element_list:
        return "PASS"
    else:
        return "FAIL"


def run_test(test):
    name = test["name"]
    check_type = test["check_type"]

    if check_type == "title":
        result = check_title(test["actual"], test["expected"])
    elif check_type == "element":
        result = check_element_exists(test["elements"], test["target"])
    else:
        result = "SKIP"

    return {"name": name, "result": result}


# --- Test Scenarios (Data Structures) ---
test_scenarios = [
    {
        "name": "Page title matches expected",
        "check_type": "title",
        "actual": "TodoMVC",
        "expected": "TodoMVC",
    },
    {
        "name": "Page title does not match",
        "check_type": "title",
        "actual": "Wrong Title",
        "expected": "TodoMVC",
    },
    {
        "name": "Submit button exists on page",
        "check_type": "element",
        "elements": ["header", "input", "submit", "footer"],
        "target": "submit",
    },
    {
        "name": "Login link exists on page",
        "check_type": "element",
        "elements": ["header", "input", "submit", "footer"],
        "target": "login",
    },
]

# --- Loop Through Tests ---
results = []

for test in test_scenarios:
    result = run_test(test)
    results.append(result)

# --- Print Report ---
print("=" * 40)
print(f"TEST REPORT - {app_name}")
print(f"URL: {base_url}")
print("=" * 40)

for result in results:
    print(f"  [{result['result']}] {result['name']}")

print("=" * 40)

pass_count = 0
fail_count = 0

for result in results:
    if result["result"] == "PASS":
        pass_count = pass_count + 1
    else:
        fail_count = fail_count + 1

print(f"  {pass_count} passed, {fail_count} failed")
print("=" * 40)

Your test runner is complete. You have a script that stores test data, processes every scenario through reusable functions, and prints a clean pass/fail report. These are the exact building blocks that real frameworks like pytest use under the hood. Next, you will extend this script with a bonus challenge.

Secret mission

Add Imports, a Contains Check, and a Timestamped Summary

Your test runner works, but it looks like a rough draft. Real test reports show when they ran, use visual symbols for quick scanning, and tell you at a glance whether anything failed. Can you add these finishing touches?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project folder or delete it.

Resources you used:

  • The python-test-runner folder on your Desktop (contains test_runner.py).

Keep everything running

No action needed. Choose this if you plan to keep experimenting with your test runner script or want to reference it when learning pytest-playwright next.

  • Your python-test-runner folder stays on your Desktop, ready to open any time.

Pause - I'll come back to this later

Since this is a local project with no running processes, there is nothing to pause. Your files will be waiting for you whenever you return.

  • No action required.

Delete - I don't want to use this again

Remove all project files from your machine.

  • Close test_runner.py in your code editor if it is still open.
  • Delete the project folder by running one of the following commands in your terminal.

macOS

rm -rf ~/Desktop/python-test-runner

Windows

Remove-Item -Recurse -Force ~\Desktop\python-test-runner
  • Confirm the folder is gone by checking your Desktop. You should no longer see python-test-runner listed.

Nice Work!

Nice Work!

You just built a working Python test runner from scratch. It processes test scenarios, routes them through check functions, and prints a formatted pass/fail report to your terminal.

You've learned how to:

  • Store test data in variables and dictionaries, then format output with f-strings.
  • Create reusable functions with parameters and return values that mirror real assertion logic.
  • Route logic with if/elif/else conditionals and process lists of test scenarios with for loops.
  • Secret Mission: Added imports, a contains check type, and a timestamped summary report with pass/fail counters.

Ready to quiz yourself?