Day 25: Generators & Iterators


 Day 25: Generators & Iterators


1. Learning Objectives

By the end of this lesson, you will be able to:

  • Understand what iterators and iterables are in JavaScript
  • Create custom iterables by implementing the Symbol.iterator method
  • Define generator functions using function* syntax
  • Use yield to produce values lazily from generators
  • Consume generators with for...of, spread operator, and destructuring
  • Delegate generators using yield*
  • Pass values back into generators using next(value)
  • Recognize practical use cases for generators (infinite sequences, async flows, data processing)

2. Theory

What are Iterators and Iterables?

An iterable is an object that defines a sequence of values that can be iterated over. JavaScript has many built-in iterables: arrays, strings, Maps, Sets, etc.

An iterator is an object that implements the iterator protocol — it has a next() method that returns an object with two properties:

  • value — the next value in the sequence
  • donetrue if the sequence is finished, false otherwise
// Manual iterator example
const array = [10, 20, 30];
const iterator = array[Symbol.iterator]();

console.log(iterator.next()); // { value: 10, done: false }
console.log(iterator.next()); // { value: 20, done: false }
console.log(iterator.next()); // { value: 30, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

The Iterable Protocol

An object is iterable if it has a method at Symbol.iterator that returns an iterator.

const iterable = {
    [Symbol.iterator]() {
        let count = 0;
        return {
            next() {
                count++;
                if (count <= 3) {
                    return { value: count * 10, done: false };
                }
                return { value: undefined, done: true };
            }
        };
    }
};

for (const value of iterable) {
    console.log(value); // 10, 20, 30
}

Generator Functions

Generator functions provide a simpler way to create iterators. They are defined with function* and use the yield keyword.

function* simpleGenerator() {
    yield 1;
    yield 2;
    yield 3;
}

const gen = simpleGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }

Key Characteristics

  • Lazy evaluation — values are produced only when requested via .next()
  • Stateful — the generator remembers its position between calls
  • Pausable — execution pauses at each yield and resumes on the next .next()
  • Two-way communication — you can pass values back into generators
// Generators are iterable
function* countUp() {
    yield 1;
    yield 2;
    yield 3;
}

// for...of works automatically
for (const num of countUp()) {
    console.log(num); // 1, 2, 3
}

// Spread works too
const numbers = [...countUp()];
console.log(numbers); // [1, 2, 3]

// Destructuring
const [a, b, c] = countUp();
console.log(a, b, c); // 1 2 3

yield* Delegation

The yield* expression delegates to another iterable or generator:

function* numbers() {
    yield 1;
    yield 2;
    yield 3;
}

function* letters() {
    yield 'A';
    yield 'B';
}

function* combined() {
    yield* numbers();
    yield* letters();
    yield* [10, 20, 30];
    yield* "Hi";
}

console.log([...combined()]);
// [1, 2, 3, 'A', 'B', 10, 20, 30, 'H', 'i']

Passing Values into Generators

You can send values back into a generator using generator.next(value). The value passed becomes the result of the yield expression inside the generator.

function* interactive() {
    const name = yield "What is your name?";
    const age = yield `Hello ${name}, how old are you?`;
    yield `So you are ${age} years old. Nice to meet you!`;
}

const gen = interactive();
console.log(gen.next());          // { value: "What is your name?", done: false }
console.log(gen.next("Alice"));   // { value: "Hello Alice, how old are you?", done: false }
console.log(gen.next(28));        // { value: "So you are 28 years old...", done: false }
console.log(gen.next());          // { value: undefined, done: true }

Infinite Generators

Generators can produce infinite sequences because they are lazy:

function* fibonacci() {
    let a = 0, b = 1;
    while (true) {
        yield a;
        [a, b] = [b, a + b];
    }
}

const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
console.log(fib.next().value); // 3
console.log(fib.next().value); // 5
// ... can continue forever

// Take only first 10
const first10 = [];
for (const n of fibonacci()) {
    if (first10.length >= 10) break;
    first10.push(n);
}
console.log(first10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

return() and throw() Methods

Generators also have return() and throw() methods:

function* countdown(start) {
    while (start > 0) {
        yield start;
        start--;
    }
}

const gen = countdown(5);
console.log(gen.next());     // { value: 5, done: false }
console.log(gen.next());     // { value: 4, done: false }
console.log(gen.return(99)); // { value: 99, done: true } — terminates early
console.log(gen.next());     // { value: undefined, done: true }

3. Code Examples

Example 1: Creating Custom Iterables

// ---- Custom Range Iterable ----
class Range {
    constructor(start, end, step = 1) {
        this.start = start;
        this.end = end;
        this.step = step;
    }

    [Symbol.iterator]() {
        let current = this.start;
        const end = this.end;
        const step = this.step;

        return {
            next() {
                if ((step > 0 && current <= end) || (step < 0 && current >= end)) {
                    const value = current;
                    current += step;
                    return { value, done: false };
                }
                return { value: undefined, done: true };
            }
        };
    }
}

// Usage
console.log("Range 1 to 5:");
for (const n of new Range(1, 5)) {
    console.log(n); // 1, 2, 3, 4, 5
}

console.log("\nRange 10 to 0 (step -2):");
for (const n of new Range(10, 0, -2)) {
    console.log(n); // 10, 8, 6, 4, 2, 0
}

// Spread works
console.log("\nSpread range:", [...new Range(3, 8)]); // [3, 4, 5, 6, 7, 8]

// ---- Custom Pagination Iterable ----
class PaginatedAPI {
    constructor(baseUrl, pageSize = 10) {
        this.baseUrl = baseUrl;
        this.pageSize = pageSize;
        this.cache = new Map();
    }

    async fetchPage(page) {
        if (this.cache.has(page)) return this.cache.get(page);
        const response = await fetch(`${this.baseUrl}?_page=${page}&_limit=${this.pageSize}`);
        const data = await response.json();
        this.cache.set(page, data);
        return data;
    }

    [Symbol.asyncIterator]() {
        let page = 1;
        let hasMore = true;
        return {
            next: async () => {
                if (!hasMore) return { done: true };
                const data = await this.fetchPage(page);
                page++;
                if (data.length === 0) {
                    hasMore = false;
                    return { done: true };
                }
                return { value: data, done: false };
            }
        };
    }
}

Example 2: Generator Basics

// ---- Basic Generator Examples ----

function* idGenerator() {
    let id = 1;
    while (true) {
        yield id++;
    }
}

const ids = idGenerator();
console.log(ids.next().value); // 1
console.log(ids.next().value); // 2
console.log(ids.next().value); // 3

// Generator that yields from an array
function* arrayIterator(arr) {
    for (let i = 0; i < arr.length; i++) {
        yield arr[i];
    }
}

const names = ["Alice", "Bob", "Charlie"];
for (const name of arrayIterator(names)) {
    console.log(name); // Alice, Bob, Charlie
}

// Generator with multiple yields
function* threeActs() {
    yield "Act 1: Setup";
    yield "Act 2: Confrontation";
    yield "Act 3: Resolution";
}

const play = threeActs();
console.log([...play]); // ["Act 1: Setup", "Act 2: Confrontation", "Act 3: Resolution"]

Example 3: Generator Delegation with yield*

// ---- yield* Delegation Examples ----

function* vegetables() {
    yield "Carrot";
    yield "Broccoli";
    yield "Spinach";
}

function* fruits() {
    yield "Apple";
    yield "Banana";
    yield "Cherry";
}

function* groceryList() {
    yield "=== Vegetables ===";
    yield* vegetables();
    yield "";
    yield "=== Fruits ===";
    yield* fruits();
    yield "";
    yield "=== Dairy ===";
    yield* ["Milk", "Cheese", "Yogurt"];
}

console.log("Grocery List:");
for (const item of groceryList()) {
    console.log(item);
}
// === Vegetables ===
// Carrot
// Broccoli
// Spinach
//
// === Fruits ===
// Apple
// Banana
// Cherry
//
// === Dairy ===
// Milk
// Cheese
// Yogurt

// ---- Deep tree traversal with yield* ----
const tree = {
    name: "Root",
    children: [
        {
            name: "Child 1",
            children: [
                { name: "Grandchild 1.1", children: [] },
                { name: "Grandchild 1.2", children: [] }
            ]
        },
        {
            name: "Child 2",
            children: [
                { name: "Grandchild 2.1", children: [] }
            ]
        }
    ]
};

function* traverseTree(node) {
    yield node.name;
    for (const child of node.children) {
        yield* traverseTree(child);
    }
}

console.log("\nTree traversal:");
for (const name of traverseTree(tree)) {
    console.log(`  ${name}`);
}
// Root, Child 1, Grandchild 1.1, Grandchild 1.2, Child 2, Grandchild 2.1

Example 4: Two-Way Communication with Generators

// ---- Interactive Generator ----

function* quizGame() {
    const answer1 = yield "What is 2 + 2?";
    if (answer1 === "4") {
        yield "Correct! Next question...";
    } else {
        yield "Wrong! The answer was 4. Next question...";
    }

    const answer2 = yield "What is the capital of France?";
    if (answer2.toLowerCase() === "paris") {
        yield "Correct! You're doing great!";
    } else {
        yield "The capital is Paris. Keep trying!";
    }

    yield "Quiz complete! Thanks for playing.";
}

const game = quizGame();
console.log(game.next().value);           // "What is 2 + 2?"
console.log(game.next("4").value);        // "Correct! Next question..."
console.log(game.next("Paris").value);    // "What is the capital of France?"
console.log(game.next("paris").value);    // "Correct! You're doing great!"
console.log(game.next().value);           // "Quiz complete! Thanks for playing."

// ---- Data Processor with Two-Way Communication ----
function* dataProcessor() {
    let total = 0;
    let count = 0;

    while (true) {
        const value = yield { total, count, average: count > 0 ? total / count : 0 };
        if (value === null) break; // Signal to stop
        total += value;
        count++;
    }

    return { finalTotal: total, finalCount: count };
}

const processor = dataProcessor();
console.log(processor.next());        // { value: { total: 0, count: 0, average: 0 }, done: false }
console.log(processor.next(10));      // { value: { total: 10, count: 1, average: 10 }, done: false }
console.log(processor.next(20));      // { value: { total: 30, count: 2, average: 15 }, done: false }
console.log(processor.next(30));      // { value: { total: 60, count: 3, average: 20 }, done: false }
console.log(processor.next(null));    // { value: { finalTotal: 60, finalCount: 3 }, done: true }

Example 5: Practical Generator Use Cases

// ---- 1. Infinite Sequence: Unique ID Generator ----
function* uniqueIdGenerator(prefix = "id") {
    let counter = 0;
    while (true) {
        yield `${prefix}_${counter++}`;
    }
}

const ids = uniqueIdGenerator("user");
console.log(ids.next().value); // "user_0"
console.log(ids.next().value); // "user_1"
console.log(ids.next().value); // "user_2"

// ---- 2. Pagination Helper ----
function* paginate(array, pageSize) {
    let index = 0;
    while (index < array.length) {
        yield array.slice(index, index + pageSize);
        index += pageSize;
    }
}

const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const pages = paginate(items, 3);

for (const page of pages) {
    console.log("Page:", page);
}
// Page: [1, 2, 3]
// Page: [4, 5, 6]
// Page: [7, 8, 9]
// Page: [10]

// ---- 3. Lazy Sequence: Even Numbers ----
function* evenNumbers() {
    let n = 0;
    while (true) {
        yield n;
        n += 2;
    }
}

// Take first 5 even numbers
const firstFive = [];
for (const even of evenNumbers()) {
    if (firstFive.length >= 5) break;
    firstFive.push(even);
}
console.log("First 5 evens:", firstFive); // [0, 2, 4, 6, 8]

// ---- 4. Async Generator: Simulated Data Stream ----
async function* dataStream() {
    let i = 0;
    while (i < 5) {
        await new Promise(resolve => setTimeout(resolve, 500));
        yield `Data chunk ${i++}`;
    }
}

async function consumeStream() {
    for await (const chunk of dataStream()) {
        console.log("Received:", chunk);
    }
    console.log("Stream complete!");
}

// consumeStream(); // Uncomment to run (takes ~2.5 seconds)

// ---- 5. Generator as State Machine ----
function* trafficLight() {
    while (true) {
        yield "🟢 Green";
        yield "🟡 Yellow";
        yield "🔴 Red";
    }
}

const light = trafficLight();
console.log(light.next().value); // 🟢 Green
console.log(light.next().value); // 🟡 Yellow
console.log(light.next().value); // 🔴 Red
console.log(light.next().value); // 🟢 Green (cycles)

4. Exercises

Beginner

  1. Create a generator function countToThree that yields 1, 2, 3. Iterate over it with for...of.
  2. Create a generator range(start, end) that yields numbers from start to end (inclusive).
  3. Use yield* to delegate to an array inside a generator.

Intermediate

  1. Write a generator fibonacci(n) that yields the first n Fibonacci numbers.
  2. Create a generator take(n, iterable) that yields the first n values from any iterable.
  3. Write a generator cycle(iterable) that infinitely cycles through the values of an iterable. (Be careful with infinite loops!)

Advanced

  1. Implement a lazy map generator: function* lazyMap(iterable, fn) that yields transformed values one at a time without creating an intermediate array.
  2. Build a simple reactive stream using generators: create a Subject that maintains a list of generator observers and pushes values to all of them.

5. Mini Project: Lazy Data Processing Pipeline

Build a data processing pipeline using generators for lazy evaluation — values flow through the pipeline one at a time without creating intermediate arrays.

// === Lazy Data Processing Pipeline ===

// ---- Generator Utilities ----

function* range(start, end, step = 1) {
    for (let i = start; i <= end; i += step) {
        yield i;
    }
}

function* map(iterable, fn) {
    for (const value of iterable) {
        yield fn(value);
    }
}

function* filter(iterable, predicate) {
    for (const value of iterable) {
        if (predicate(value)) {
            yield value;
        }
    }
}

function* take(iterable, count) {
    let taken = 0;
    for (const value of iterable) {
        if (taken >= count) return;
        yield value;
        taken++;
    }
}

function* skip(iterable, count) {
    let skipped = 0;
    for (const value of iterable) {
        if (skipped >= count) {
            yield value;
        }
        skipped++;
    }
}

function* enumerate(iterable) {
    let index = 0;
    for (const value of iterable) {
        yield [index, value];
        index++;
    }
}

function reduce(iterable, fn, initial) {
    let accumulator = initial;
    let first = true;
    for (const value of iterable) {
        if (first && initial === undefined) {
            accumulator = value;
            first = false;
        } else {
            accumulator = fn(accumulator, value);
        }
    }
    return accumulator;
}

function toArray(iterable) {
    return [...iterable];
}

// ---- Pipeline Demo ----

console.log("=== Lazy Data Processing Pipeline ===\n");

// Build a pipeline: range(1, 100) → filter even → map square → take 5
const pipeline = take(
    map(
        filter(range(1, 100), n => n % 2 === 0),
        n => n * n
    ),
    5
);

console.log("Pipeline results (first 5 even squares from 1-100):");
for (const value of pipeline) {
    console.log(`  ${value}`);
}
// 4, 16, 36, 64, 100

// ---- More Complex Pipeline ----

console.log("\n--- Complex Pipeline: Process user data ---\n");

// Simulated large dataset (would be 1M+ in real use)
function* generateUsers(count) {
    const firstNames = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank"];
    const lastNames = ["Smith", "Johnson", "Williams", "Brown", "Jones"];
    for (let i = 0; i < count; i++) {
        yield {
            id: i + 1,
            firstName: firstNames[i % firstNames.length],
            lastName: lastNames[i % lastNames.length],
            age: 18 + (i % 50),
            email: `user${i + 1}@example.com`
        };
    }
}

// Pipeline: generate 100 users → filter adults 18+ → map to full name + age group → skip 5 → take 3
const adultUsers = take(
    skip(
        map(
            filter(generateUsers(100), user => user.age >= 18),
            user => ({
                fullName: `${user.firstName} ${user.lastName}`,
                age: user.age,
                ageGroup: user.age < 30 ? "Young Adult" :
                          user.age < 50 ? "Adult" : "Senior",
                email: user.email
            })
        ),
        5
    ),
    3
);

console.log("Processed users (skip 5, take 3):");
for (const user of adultUsers) {
    console.log(`  ${user.fullName} | ${user.age} (${user.ageGroup}) | ${user.email}`);
}

// ---- Reduce Example ----

console.log("\n--- Statistics using reduce on pipeline ---");

const stats = reduce(
    take(
        map(
            filter(range(1, 1000), n => n % 2 !== 0), // odd numbers
            n => n * n // squared
        ),
        50 // first 50 odd squares
    ),
    (acc, val) => ({
        sum: acc.sum + val,
        count: acc.count + 1,
        min: Math.min(acc.min, val),
        max: Math.max(acc.max, val)
    }),
    { sum: 0, count: 0, min: Infinity, max: -Infinity }
);

console.log("Statistics of first 50 odd squares:");
console.log(`  Count: ${stats.count}`);
console.log(`  Sum: ${stats.sum}`);
console.log(`  Min: ${stats.min}`);
console.log(`  Max: ${stats.max}`);
console.log(`  Average: ${(stats.sum / stats.count).toFixed(2)}`);

// ---- Lazy vs Eager Performance Comparison ----

console.log("\n--- Performance: Lazy vs Eager ---\n");

// Eager approach (creates intermediate arrays)
function eagerProcess() {
    const range = [];
    for (let i = 1; i <= 10000; i++) range.push(i);

    const evens = range.filter(n => n % 2 === 0);
    const squared = evens.map(n => n * n);
    const result = squared.slice(0, 5);
    return result;
}

// Lazy approach (no intermediate arrays)
function lazyProcess() {
    return take(
        map(
            filter(range(1, 10000), n => n % 2 === 0),
            n => n * n
        ),
        5
    );
}

console.time("Eager (creates arrays)");
const eagerResult = eagerProcess();
console.timeEnd("Eager (creates arrays)");

console.time("Lazy (generators)");
const lazyResult = [...lazyProcess()];
console.timeEnd("Lazy (generators)");

console.log("Both produce:", eagerResult, lazyResult);

Try extending it: Add zip, flatMap, distinct, chunk, sort (with buffer), or groupBy as generator utilities. Benchmark larger datasets to see the memory difference.


6. Common Mistakes

MistakeWhy it's wrongCorrect approach
Forgetting * in function*Creates a regular function instead of a generatorAlways use function* name() {}
Using return instead of yieldreturn ends the generator immediately with done: trueUse yield to produce values; use return only for final value
Trying to reuse a generatorA generator can only be iterated once; subsequent calls return emptyCreate a new generator instance each time
Expecting generators to be async by defaultGenerators are synchronous unless explicitly defined as asyncUse async function*() and for await...of for async
Modifying the underlying data while iteratingCan cause unexpected behavior or skipped valuesIterate over a copy if data might change
Using yield inside a callback or arrow functionyield is only valid inside generator functionsEnsure the function containing yield is declared with function*
Not handling infinite generators carefullyfor...of on an infinite generator loops foreverUse take(), break, or return() to terminate
Forgetting that [...gen] consumes the entire generatorFor infinite generators, this causes an infinite loopUse take() or a loop with break for partial consumption

7. Best Practices

  • Use generators for lazy sequences — when you don't want to compute or store all values at once.
  • Use generators for infinite sequences — mathematical sequences, unique IDs, cyclic patterns.
  • Use generators for state machines — traffic lights, game states, workflow steps.
  • Use yield* for delegation — compose multiple generators or iterate over nested structures.
  • Use generators for data processing pipelines — chain map, filter, take without intermediate arrays.
  • Always handle infinite generators with a take() utility or explicit break condition.
  • Create a new generator instance each time you need to iterate from the beginning.
  • Use for...of for clean consumption — it calls .next() and checks done automatically.
  • Use async generators for streaming data — API pagination, file reading, WebSocket messages.
  • Name your generators clearly — use noun-based names like idGenerator, fibonacciSequence.

8. Challenge Assignment (Optional)

"Async Data Stream Processor" Challenge

Build an async generator-based system that processes a stream of data from multiple sources.

Requirements:

  1. Create an async function* dataSource(url, interval) that:

    • Simulates fetching data from a URL at a given interval
    • Yields each chunk of data as it arrives
    • Handles errors gracefully (logs and continues)
  2. Create async generator transforms:

    • filterAsync(source, predicate) — async filter
    • mapAsync(source, transform) — async map
    • batch(source, size) — collects items into batches of size
    • debounce(source, ms) — only yields after ms of inactivity
  3. Create a StreamProcessor class that:

    • Connects multiple sources and transforms
    • Provides pipe(transform) method for chaining
    • Provides subscribe(callback) to consume the final output
    • Provides start() and stop() methods
    • Tracks statistics (items processed, errors, throughput)

Bonus:

  • Add a retry mechanism for failed source fetches
  • Add a rateLimit transform
  • Implement backpressure handling
  • Visualize the stream in a browser UI with real-time updates

Example usage:

const processor = new StreamProcessor()
    .fromSource(dataSource("/api/events", 1000))
    .pipe(filterAsync(event => event.type === "user_action"))
    .pipe(mapAsync(event => ({ ...event, timestamp: Date.now() })))
    .pipe(batch(10))
    .pipe(debounce(500))
    .subscribe(batch => console.log("Batch received:", batch));

processor.start();
// After 30 seconds:
processor.stop();
console.log(processor.getStats());
// { received: 30, processed: 22, errors: 3, throughput: 0.73 items/sec }

9. Knowledge Check (Quiz)

  1. What does a generator function return when called?

    • a) An array of yielded values
    • b) An iterator object with a next() method
    • c) The first yielded value
    • d) undefined
  2. Which keyword is used to produce values from a generator?

    • a) return
    • b) emit
    • c) yield
    • d) produce
  3. What is the output of this code?

    function* gen() {
        yield 1;
        yield 2;
    }
    const g = gen();
    console.log(g.next());
    console.log(g.next());
    console.log(g.next());
    
    • a) 1, 2, undefined
    • b) { value: 1, done: false }, { value: 2, done: false }, { value: undefined, done: true }
    • c) { value: 1, done: true }, { value: 2, done: true }, { value: undefined, done: true }
    • d) [1, 2], [], []
  4. What does yield* do?

    • a) Yields the current value multiplied by itself
    • b) Delegates to another iterable or generator
    • c) Yields all remaining values at once
    • d) Stops the generator
  5. How do you pass a value back into a generator?

    • a) generator.send(value)
    • b) generator.next(value)
    • c) generator.emit(value)
    • d) generator.yield(value)
  6. What is the key advantage of generators over arrays for large sequences?

    • a) Generators are faster
    • b) Generators are lazy — they don't store all values in memory
    • c) Generators support async operations
    • d) Both b and c
  7. What happens if you iterate over an infinite generator with for...of?

    • a) It runs forever (infinite loop)
    • b) It stops after 10000 iterations
    • c) It throws an error
    • d) It only processes the first value
  8. How do you consume an async generator?

    • a) for (const value of gen) {}
    • b) for await (const value of gen) {}
    • c) while (gen.next()) {}
    • d) gen.forEach(value => {})

Answers: 1-b, 2-c, 3-b, 4-b, 5-b, 6-d, 7-a, 8-b


10. Additional Resources


11. Summary

Today you learned:

  • Iterables and iterators — objects that define sequences via Symbol.iterator and the next() method
  • Generator functionsfunction* with yield for creating iterators more easily
  • Lazy evaluation — values are produced only when requested, saving memory
  • yield* delegation — composing generators and delegating to other iterables
  • Two-way communication — passing values into generators via next(value)
  • Infinite generators — sequences that never end (Fibonacci, IDs, counters)
  • Generator methodsreturn() and throw() for early termination and error injection
  • Async generatorsasync function* with for await...of for streaming data
  • Practical applications — unique IDs, pagination, lazy data pipelines, state machines, data streams

You now have a powerful tool for lazy, memory-efficient data processing. Generators bridge the gap between simple arrays and complex asynchronous streams. Tomorrow we'll dive into the Event Loop and concurrency — understanding how JavaScript handles multiple tasks behind the scenes.


"Generators are the lazy bones of JavaScript — they only work when you ask them to. And that's exactly what makes them powerful."

Comments

EARN AT THE COMFORT OF YOUR HOME

Sponsored content

Popular posts from this blog

Day 5 – Functions, Scope, and Docstrings

Day 10 – Exception Handling and Debugging

Day 1 – Python Foundations for AEC Professionals