Exploring Python Concepts with One Program

I'm passionate about coding and enjoy working with embedded systems, app development, and compiler design.
While learning Python, I noticed that most examples explain only one concept at a time. To understand how different features work together, I wrote a small program that combines multiple Python topics into a single example. Even though the program is simple, it demonstrates asynchronous programming, decorators, generators, tasks, user input, and concurrency.
In this blog, I'll explain each concept used in the program and how they interact with each other.
Program Overview
The main goal of this program is to perform two tasks simultaneously:
Generate Fibonacci numbers.
Ask the user for their name and modify the output using a decorator.
Instead of executing these tasks one after another, they run concurrently using Python's asyncio library.
1. Decorators
The first concept used is a decorator.
def fake(func):
async def change(*args, **kwargs):
res = await func(*args, **kwargs)
res = "Crazy " + res
return res
return change
A decorator allows us to modify the behavior of another function without changing its original code.
Here, the fake decorator wraps the hello() function. Whatever name the user enters gets modified by adding the word "Crazy" before returning it.
For example:
Input: Gagan
Output: Crazy Gagan
This is a simple example of how decorators can add extra functionality while keeping the original function clean.
2. Asynchronous Functions
The program uses asynchronous functions with the async keyword.
@fake
async def hello():
and
async def fibo():
An asynchronous function doesn't immediately execute everything line by line. Instead, it can pause whenever it reaches an await statement and allow another task to run during that time.
This helps when dealing with operations that take time, such as waiting for user input or network requests.
3. Using await
Inside the program, await appears several times.
he = await asyncio.to_thread(input, "Enter your name:")
Normally, input() blocks the entire program until the user types something. Since input() is not asynchronous, I used asyncio.to_thread() to run it in a separate thread so that it doesn't stop other asynchronous tasks.
Another example is
await asyncio.sleep(0.1)
Unlike time.sleep(), this pauses only the current coroutine instead of blocking the whole program.
Similarly,
await asyncio.sleep(3)
inside fibo() delays Fibonacci generation without freezing the event loop.
4. Generators
The Fibonacci sequence is generated using a generator function.
def fib(n):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
Instead of storing all Fibonacci numbers in a list, the generator produces one value at a time using the yield keyword.
This is memory-efficient because only one number exists in memory during each iteration.
The generator is later used like this:
for num in fib(10):
print(num)
which prints the first ten Fibonacci numbers.
5. Creating Tasks
Inside main(), I created two asynchronous tasks.
task1 = asyncio.create_task(fibo())
task2 = asyncio.create_task(hello())
create_task() schedules both coroutines to run concurrently.
Without create_task(), the program would wait for one coroutine to finish before starting the other.
6. Waiting for Tasks
The results are collected using
ans1 = await task1
ans2 = await task2
await pauses only until the specific task finishes and returns its value.
Later, I also used
res = await asyncio.gather(fibo(), hello())
asyncio.gather() executes multiple coroutines together and returns all their results in a list.
This is useful when we want to wait for several asynchronous operations simultaneously.
7. Event Loop
The program starts with
if __name__ == "__main__":
asyncio.run(main())
asyncio.run() creates the event loop, executes the main() coroutine, and closes the loop after completion.
The event loop is responsible for scheduling and switching between asynchronous tasks whenever they reach an await statement.
8. Return Values
The two asynchronous functions return different values.
hello() returns the modified username after passing through the decorator.
fibo() prints the Fibonacci sequence and returns the last generated number.
Finally,
print(ans1, ans2, "async tasks performed", res)
prints the results obtained from both tasks.
Concepts Covered
This single program demonstrates several important Python topics:
Decorators
Asynchronous programming (
asyncandawait)asyncio.create_task()asyncio.gather()Event loop using
asyncio.run()Running blocking functions with
asyncio.to_thread()Generators and the
yieldkeywordFibonacci sequence generation
Concurrent execution of multiple tasks
import time
import asyncio
import functools
def fake(func):
async def change(*args,**kwargs):
res=await func(*args,**kwargs)
res="Crazy"+" "+res
return res
return change
@fake
async def hello():
he=await asyncio.to_thread(input,"Enter your name:")
await asyncio.sleep(0.1)
#he="Yello"
return he
def fib(n):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
async def fibo():
await asyncio.sleep(3)
nun=0
for num in fib(10):
print(num)
return num
async def main():
task1=asyncio.create_task(fibo())
task2=asyncio.create_task(hello())
ans1=await task1
ans2=await task2
res=await asyncio.gather(fibo(),hello())
print(ans1,ans2,"async tasks performed",res)
if __name__=="__main__":
asyncio.run(main())
What I Learned
Writing this program helped me understand that asynchronous programming is not just about making code faster—it is about using waiting time efficiently. While one task is waiting for input or sleeping, another task can continue executing. Combining decorators, generators, and asynchronous functions in the same program also showed me how different Python features can work together in a practical example.
Happy Coding...





