How Green Is Your Code? 10 Experiments You Can Test

Editorially reviewed and edited by Brett Stadelmann.

Two pieces of code can produce exactly the same answer while placing very different demands on a computer.

One version might finish almost instantly. Another might perform thousands of unnecessary calculations, allocate far more memory, transfer more data or keep the processor busy for several extra seconds. When software is used repeatedly or scaled across many devices, those differences begin to matter.

This is where energy-efficient programming becomes practical. Instead of simply memorising a list of energy-efficient coding principles, you can test different approaches and see the effects for yourself.

In this beginner-friendly coding lab, you’ll compare ten common programming choices. You don’t need specialised hardware or advanced knowledge. A computer and Python are enough for most exercises. The API exercise also uses the requests package and a local test service; the SQL exercise needs a test database. Start with the self-contained Python examples.

Table of Contents

What Does It Mean for Code to Be Green?

Green code performs a useful task while avoiding unnecessary energy use, hardware demand and carbon emissions.

That does not mean the shortest piece of code is automatically the greenest. A compact line can still trigger a slow algorithm, make excessive network requests or load far more information than the user needs.

The Software Carbon Intensity specification developed by the Green Software Foundation describes three broad ways software can reduce emissions:

  • Energy efficiency: using less electricity to perform the same function
  • Hardware efficiency: making better use of existing physical resources
  • Carbon awareness: running flexible workloads at times or in locations where electricity has a lower carbon intensity

These ideas apply to complete software systems, not just individual lines of code. A program may depend on processors, memory, storage, networks, databases, cloud servers and the user’s device. All of those components can affect its environmental footprint.

If this is a new topic for you, start with the broader guide to green software engineering before attempting the experiments below.

Can You Measure the Energy Used by Code?

Yes, but accurate energy measurement is more complicated than checking how long a program takes to run.

Execution time is a useful starting point because a processor that remains active for longer will often consume more energy. However, runtime alone does not capture everything. Two programs can place different demands on the processor, memory, storage and network, even when they take a similar amount of time.

Professional researchers may use processor energy counters, external power meters, operating-system telemetry or dedicated laboratory equipment. Beginners can start with simpler measurements:

  • Execution time
  • Peak memory use
  • CPU utilisation
  • Number of database or network requests
  • Amount of data transferred
  • Battery use during longer tests

These measurements do not give you a complete carbon footprint, but they can reveal computational waste and help you compare two implementations of the same task.

Set Up a Simple Python Testing Lab

The experiments below use Python because it is approachable and widely used by beginners. You can still apply the underlying lessons to other programming languages.

First, confirm that Python 3 is installed. Then create a new file named green_code_lab.py.

You can measure execution time with Python’s built-in timeit module:

from timeit import timeit

runtime = timeit(
    "sum(range(1_000_000))",
    number=100
)

print(f"Runtime: {runtime:.6f} seconds")

The number value tells Python how many times to repeat the operation. Repetition is important because a tiny task may finish too quickly to measure reliably on its own.

For a separate memory test, use Python’s built-in tracemalloc. It tracks Python memory allocations made while tracing is active, not all process memory or electricity use. Keep memory profiling separate from timing because tracing adds overhead.

import tracemalloc

tracemalloc.start()
values = [n * n for n in range(100_000)]
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"Peak traced allocation: {peak / 1024:.1f} KiB")

Keep Your Tests Fair

Small differences in background activity can distort results. For more useful comparisons:

  • Run both versions on the same computer.
  • Close unnecessary applications and browser tabs.
  • Use the same input data for each version.
  • Repeat each test several times.
  • Alternate the order in which you test the versions.
  • Record the median result rather than trusting one run.
  • Confirm that both versions produce the same correct output.

A faster result is meaningless if the supposedly efficient version solves a different problem or produces the wrong answer.

Experiment 1: List Search Versus Set Lookup

Imagine that you have a large collection of usernames and need to check whether a particular name appears in it.

A list searches through its contents until it finds a match. A set uses a hash-based lookup that is generally much faster for repeated membership checks.

Version A: Search a List

usernames_list = [f"user_{i}" for i in range(100_000)]

def find_with_list():
    return "user_99999" in usernames_list

Version B: Search a Set

usernames_set = set(usernames_list)

def find_with_set():
    return "user_99999" in usernames_set

Keep both snippets in the same file. These separate variable names ensure each function tests the intended data structure. The harness below times repeated lookups; it excludes building the list and set. Also measure construction separately when it is part of your real workload.

from statistics import median
from timeit import repeat

assert find_with_list() is True
assert find_with_set() is True

for name, function in [("list", find_with_list), ("set", find_with_set)]:
    timings = repeat(function, number=100, repeat=5)
    print(name, "median seconds per lookup:", median(timings) / 100)

This follows the callable interface of Python’s timeit module. Reverse the order for another run, record the Python version and hardware, and test missing values as well as values near the start and end of the list. Runtime results are not energy measurements.

What to investigate: The set should make repeated membership checks substantially faster, but it may also require more memory. This demonstrates an important green coding principle: optimisation often involves trade-offs rather than one universally superior choice.

A list may still be appropriate when order matters, memory is limited or you only need to scan the data once.

Experiment 2: Recalculating Versus Caching

Programs often repeat the same expensive calculation even though the answer has not changed. Caching stores a previous result so it can be reused.

Version A: Repeat Every Calculation

def fibonacci_plain(n):
    if n < 2:
        return n
    return fibonacci_plain(n - 1) + fibonacci_plain(n - 2)

assert fibonacci_plain(28) == 317811

Version B: Cache Previous Results

from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci_cached(n):
    if n < 2:
        return n
    return fibonacci_cached(n - 1) + fibonacci_cached(n - 2)

assert fibonacci_cached(28) == 317811

def cold_cached_call():
    fibonacci_cached.cache_clear()
    return fibonacci_cached(28)

def warm_cached_call():
    return fibonacci_cached(28)

What to investigate: Compare fibonacci_plain(28), cold_cached_call() and warm_cached_call() separately. Populate the cache once before timing the warm case. Clearing the cache is included in the cold wrapper, so record that boundary. Use small repeat counts for the uncached recursion and keep printing outside the timed work.

Caching can reduce processor use, server load and response times. However, cached information also occupies memory and can become outdated. It is most useful when a result is expensive to produce, requested repeatedly and safe to reuse.

Experiment 3: A Generator Versus a Full List

Suppose you want to process one million numbers but do not need to keep all of them in memory at once.

Version A: Build the Entire List

def squared_numbers_list(limit):
    return [number * number for number in range(limit)]

for value in squared_numbers_list(1_000_000):
    pass

Version B: Produce Values as Needed

def squared_numbers_generator(limit):
    for number in range(limit):
        yield number * number

for value in squared_numbers_generator(1_000_000):
    pass

What to investigate: Measure peak memory use. A generator produces one value at a time rather than creating the entire collection in advance.

This approach can be especially useful when reading large files, processing streams or working with datasets that would otherwise consume a substantial amount of memory.

The list may still be better when you need to reuse its values, access them by position or perform several operations on the complete collection.

Experiment 4: One-Pass Processing Versus Multiple Loops

Readable code matters, and splitting work into several steps can sometimes improve clarity. However, repeatedly scanning a very large collection may also create unnecessary work.

Version A: Scan the Data Three Times

numbers = range(100_000)

even_count = sum(1 for number in numbers if number % 2 == 0)
total = sum(numbers)
maximum = max(numbers)

Version B: Collect the Results in One Pass

numbers = range(100_000)

even_count = 0
total = 0
maximum = None

for number in numbers:
    total += number

    if number % 2 == 0:
        even_count += 1

    if maximum is None or number > maximum:
        maximum = number

What to investigate: Compare runtime and readability. The one-pass version avoids repeatedly traversing the range, but it is not guaranteed to be faster: Python built-ins may execute more work in optimised native code than a hand-written loop. Compare the resulting count, sum and maximum before interpreting the timings.

This experiment is a reminder that green software engineering should not produce fragile or incomprehensible code. Maintainability, correctness, security and accessibility still matter.

Experiment 5: Efficient Sorting Versus Bubble Sort

Bubble sort is often taught because it is easy to understand, not because it is an efficient way to sort large datasets.

Version A: Bubble Sort

def bubble_sort(values):
    values = values.copy()

    for end in range(len(values) - 1, 0, -1):
        for index in range(end):
            if values[index] > values[index + 1]:
                values[index], values[index + 1] = (
                    values[index + 1],
                    values[index]
                )

    return values

Version B: Python’s Built-In Sort

def built_in_sort(values):
    return sorted(values)

Start with a shuffled list of 1,000 numbers and give both functions copies of that same input. Use a fixed random seed if generating it again. Confirm both outputs equal sorted(input_values), then increase the size cautiously; bubble sort quickly becomes expensive.

What to investigate: Increase the size of the dataset gradually. The performance gap should become much more noticeable as the input grows.

Choosing an appropriate algorithm is often more consequential than making tiny improvements to individual lines of code. Before trying clever micro-optimisations, check whether the program is using a fundamentally inefficient method.

Experiment 6: Repeated File Access Versus One Read

Storage operations are usually slower and more resource-intensive than accessing information already held in memory.

Version A: Open the File Repeatedly

def count_keyword_repeatedly(filename, keyword, repeats):
    total = 0

    for _ in range(repeats):
        with open(filename, "r", encoding="utf-8") as file:
            text = file.read()
            total += text.count(keyword)

    return total

Version B: Read Once and Reuse

def count_keyword_once(filename, keyword, repeats):
    with open(filename, "r", encoding="utf-8") as file:
        text = file.read()

    count = text.count(keyword)
    return count * repeats

What to investigate: Compare the difference with a large text file and a high repeat count.

The second example is only valid when the file does not change during the test. In a real application, caching a file indefinitely could cause users to receive stale information. Efficient software still needs a sensible update strategy.

Experiment 7: Many Small Writes Versus Batched Output

Writing information to a file one small piece at a time adds repeated Python function calls. This experiment compares that overhead with building a complete output string; buffering means disk operations do not map directly to those calls.

Version A: Write Each Line Separately

def write_separately(filename, lines):
    with open(filename, "w", encoding="utf-8") as file:
        for line in lines:
            file.write(line + "\n")

Version B: Write the Lines Together

def write_in_batch(filename, lines):
    with open(filename, "w", encoding="utf-8") as file:
        file.write("".join(line + "\n" for line in lines))

What to investigate: Try both functions with hundreds of thousands of short lines. Measure runtime and peak memory.

Batching may reduce the number of storage or network operations, but very large batches can consume excessive memory or delay useful output. Production systems often use moderate batch sizes rather than collecting everything indefinitely.

Both writing versions now produce the same trailing newline for every input line. Test empty input and compare the resulting bytes. Python text files are already buffered, so one write() call is not necessarily one disk operation. The joined version also allocates the complete output string; measure that memory trade-off and use bounded chunks for large outputs.

Experiment 8: Repeated API Requests Versus Reusing Data

Every web request can involve network equipment, servers, databases and data transfer. Making the same request repeatedly creates work throughout that chain.

Use a local test endpoint returning fixed, non-sensitive JSON. The requests package is an extra dependency. Do not send 100 test requests to someone else’s service. Reuse is valid only when the answer may remain unchanged for the whole workload; copying the JSON also keeps results independently mutable. Test equality and freshness requirements before counting avoided requests.

Keep the endpoint and returned data fixed for both versions, and count the requests as well as timing the workload.

Version A: Request the Same Data Repeatedly

import requests

def repeated_requests(url):
    results = []

    for _ in range(100):
        response = requests.get(url, timeout=10)
        response.raise_for_status()
        results.append(response.json())

    return results

Version B: Request Once and Reuse

import requests

def reused_request(url):
    response = requests.get(url, timeout=10)
    response.raise_for_status()
    data = response.json()

    from copy import deepcopy
    return [deepcopy(data) for _ in range(100)]

What to investigate: Record the total runtime and estimate the amount of data transferred. Browser developer tools and command-line utilities can help you inspect response sizes.

Real applications need rules governing how long information may be cached. Weather data, stock availability and transport schedules may require frequent updates, while a country list or historical record might remain valid much longer.

Experiment 9: Fetching Everything Versus Only What You Need

Over-fetching happens when an application retrieves more information than it needs to complete a task.

Imagine a database containing a million customer records with dozens of fields.

Version A: Retrieve Complete Records

SELECT *
FROM customers
WHERE newsletter_subscriber = TRUE
ORDER BY customer_id;

Version B: Retrieve the Required Fields

SELECT customer_id, first_name
FROM customers
WHERE newsletter_subscriber = TRUE
ORDER BY customer_id;

What to investigate: Using a local test database, compare query time, returned data size and application memory use. Both queries now select the same rows in the same order; compare the customer_id and first_name projection of the first result with the second result. Column selection is the variable under test. Keep indexes, data and cache conditions documented.

Database optimisation can reduce computation, storage reads and network transfer while also making an application feel faster. Useful techniques include selecting only required columns, adding appropriate indexes, paginating large results and avoiding repeated queries inside loops.

You can explore more approachable software in the guide to beginner green coding tools.

Experiment 10: AI-Generated Code Versus an Improved Version

AI coding assistants can produce working programs quickly, but working code is not automatically efficient code.

Treat any generated solution as a candidate implementation. Its correctness and resource use must be checked on the actual task; the source of the code does not establish either.

Choose a straightforward task, such as:

  • Finding duplicate values in a list
  • Counting word frequencies in a document
  • Locating two numbers that add to a target
  • Combining overlapping date ranges
  • Finding the most common item in a dataset

Ask an AI assistant to produce a Python solution. Confirm that it works, then inspect it for unnecessary nested loops, repeated calculations, excessive copying or inappropriate data structures.

For example, an AI might generate this duplicate finder:

def find_duplicates_baseline(values):
    duplicates = []

    for index, value in enumerate(values):
        if value in values[index + 1:] and value not in duplicates:
            duplicates.append(value)

    return duplicates

For hashable values, a counting approach preserves the baseline’s first-appearance order:

from collections import Counter

def find_duplicates_counted(values):
    counts = Counter(values)
    return [value for value, count in counts.items() if count > 1]

for values, expected in [([], []), ([1], []), ([2, 1, 1, 2], [2, 1]),
                         ([3, 3, 3], [3])]:
    assert find_duplicates_baseline(values) == expected
    assert find_duplicates_counted(values) == expected

What to investigate: Run both versions with increasingly large input lists. Check whether they produce equivalent results, then compare runtime and memory use.

The counted version requires hashable inputs, such as integers or strings, and can use more memory. Test only that agreed input domain when comparing the versions. Output order and duplicate handling are part of correctness, not details to ignore when measuring speed.

Are Some Programming Languages Greener Than Others?

You may have seen charts ranking programming languages by energy consumption. These comparisons can be interesting, but they are easy to misinterpret.

A benchmark describes the tested programs and environment. A ranking from one set of tasks cannot establish a universal ordering for every application.

However, a programming language does not run by itself. The result can be affected by:

  • The specific program being tested
  • The quality of its implementation
  • The compiler or interpreter
  • The hardware and operating system
  • The number of processor cores in use
  • Memory and storage activity
  • Compiler settings and optimisations
  • The measurement boundary

To compare implementations meaningfully, specify the same useful output, representative inputs and a consistent measurement boundary. Runtime alone still cannot establish electricity use or carbon emissions.

This does not mean language choice is irrelevant. A language and its ecosystem can make certain algorithms, optimisations and deployment models easier or harder to use. It does mean you should be sceptical of universal claims that one language is always green and another is always wasteful.

Turn Your Results Into a Green Code Scorecard

Create a spreadsheet with one row for each test and the following columns:

ExperimentVersionMedian runtimePeak memoryRequests or operationsOutput correct?Notes
List versus setList
List versus setSet

Calculate the percentage change between your baseline and improved version:

percentage_reduction = (
    (baseline - improved) / baseline
) * 100

For example, if one version takes 10 seconds and another takes 6 seconds:

print(((10 - 6) / 10) * 100)  # 40.0 percent

The second version reduces runtime by 40% under those particular test conditions.

Use careful language when discussing your findings. Rather than writing “sets use less energy than lists,” write:

In this membership-check experiment, the set-based version completed the defined workload faster on the tested computer. It also used more memory, so the best choice depends on how the data will be used.

That conclusion is less dramatic, but it is much more accurate.

How to Estimate Carbon Emissions

Once you have an energy estimate, operational carbon emissions can be approximated by multiplying the electricity consumed by the carbon intensity of the electricity supply:

Operational emissions = Energy consumed × Grid carbon intensity

For example, a task that consumes 0.001 kilowatt-hours in a region with a grid intensity of 400 grams of carbon dioxide equivalent per kilowatt-hour would produce an estimated:

0.001 kWh × 400 gCO2e/kWh = 0.4 gCO2e

This figure only covers the boundary you measured. It may exclude networking equipment, cooling, idle capacity, hardware manufacturing and other supporting infrastructure.

The full Software Carbon Intensity method also considers embodied emissions from hardware and expresses emissions per functional unit, such as one user, transaction, API call or completed job.

For a beginner experiment, the most important principle is consistency. Use the same method, computer, workload and assumptions when comparing the baseline and improved versions.

Common Measurement Mistakes

Testing Different Outputs

Both programs must complete the same useful work. A version that processes half the data is not more efficient; it is performing a smaller task.

Relying on One Run

Operating systems perform updates, antivirus scans and background maintenance. One unusually fast or slow result may not represent typical performance.

Ignoring Idle Consumption

Your computer uses electricity before the test begins. State whether you report total device energy or an estimate above an idle baseline. If subtracting a baseline, measure it under comparable conditions, document the method and retain the total result too. A shorter run also changes how long baseline power is consumed; do not casually mix total and incremental measurements.

Optimising Tiny Tasks With No Real Impact

Reducing a function from 0.0002 seconds to 0.0001 seconds may be irrelevant if it runs once per month. Focus on code that executes frequently, handles large datasets or operates across many devices.

Sacrificing Readability

Highly compressed or obscure code can create bugs and make future maintenance more difficult. An optimisation that saves a tiny amount of processing but requires repeated rewrites may not improve the system as a whole.

Assuming Faster Always Means Lower Carbon

A faster program may temporarily use more processor cores or specialised hardware. Carbon emissions also depend on where and when electricity is consumed. Runtime is a valuable indicator, but it is not a complete environmental assessment.

Which Optimisations Usually Matter Most?

The most useful improvements are often not clever changes to individual lines. They are larger decisions that eliminate unnecessary work.

Start by asking:

  • Can the program use a more suitable algorithm?
  • Is it repeating a calculation that could be cached?
  • Is it loading or transferring data nobody needs?
  • Does it make avoidable database or API requests?
  • Can inactive services scale down or switch off?
  • Could a flexible task run when lower-carbon electricity is available?
  • Will the software continue working on older hardware?

These questions connect code-level optimisation with the wider principles of sustainable computing.

A Beginner’s Green Coding Checklist

Before finishing a project, check whether you have:

  • Chosen an appropriate algorithm and data structure
  • Removed avoidable repeated calculations
  • Limited database queries and network requests
  • Requested only the data the user needs
  • Measured runtime using repeatable tests
  • Checked memory use as well as speed
  • Tested larger and more realistic inputs
  • Compared equivalent outputs
  • Documented the test environment and assumptions
  • Kept the code understandable and maintainable
  • Tested AI-generated code rather than trusting it automatically
  • Considered the full system, not just one function

Green Coding Begins With Curiosity

You do not need a laboratory to begin thinking more carefully about software efficiency.

Simple experiments can show how algorithms, data structures, caching, file access, network requests and database design affect the work a computer must perform. They can also teach a more important lesson: there is rarely one perfectly green coding choice.

The best solution depends on the task, scale, hardware, users and wider system. A change that reduces runtime may increase memory use. Caching may prevent repeated calculations but serve outdated information. Batching may reduce network operations while delaying results.

Green software development is therefore not about following rigid rules. It is about measuring, comparing and making informed decisions.

Run the experiments. Record what happens. Question surprising results. Then use what you learn to build software that performs useful work with less waste.

Frequently Asked Questions

Does efficient code use less electricity?

Often, but not in every situation. Code that finishes a task faster commonly requires less energy, especially when the same hardware performs the same workload. However, processor utilisation, memory, storage, networking and hardware type can also affect total electricity consumption.

What is the easiest way to measure code efficiency?

Beginners can start by comparing execution time, peak memory use, request counts and transferred data. These metrics do not provide a complete carbon footprint, but they help identify avoidable computational work.

Is Python bad for the environment?

No programming language is environmentally harmful in isolation. Python may be slower than a compiled language for some computational tasks, but software impact depends on the implementation, workload, scale, hardware and deployment. An efficient Python solution may outperform a poorly designed program written in another language.

Is AI-generated code energy efficient?

It can be, but efficiency is not guaranteed. AI assistants may produce unnecessary loops, repeated work or unsuitable data structures. Treat generated code as a draft that must be tested for correctness, security, readability and performance.

Should beginners worry about green coding?

Beginners do not need to optimise every line. Learning to recognise wasteful algorithms, unnecessary requests and repeated calculations builds good habits that improve performance as well as sustainability.

New to Python? Work through the beginner coding plan first, then return to one experiment at a time.