Part 4: Building Reusable & Resilient Scripts
Master the building blocks of clean, reusable Python code with functions, and make your scripts resilient with real-world error handling.
In Part 3, we learned how to control flow using loops, store structured data in dictionaries and tuples, and enforce uniqueness using sets. That gave our python scripts much-needed structure.
But as your scripts grow, so do the needs for
organization and resilience.
That’s where this chapter comes in. We’ll learn how to write
reusable blocks of code using functions and build
fault-tolerant logic using try-except blocks,
critical when you’re handling unpredictable real-world inputs from
APIs, knowledge users, or customers.
Whether you're validating user input in a low-code platform, integrating APIs into a webhook handler, or writing a fallback routine for a bot, these concepts will take your skills from "just writing code" to engineering reliable systems.
The embedded code editor below works great for trying things out. Type, run, break things, and fix them, that's how you grow.
- Click the hamburger menu (top-left) to open the sidebar. And you can do the following:
- Reset to start fresh anytime.
- Download to save your code. You can also download the code as a zip file.
- Fullscreen mode = more space! Press Esc to exit.
Intro & Recap
If you’ve followed along from the beginning, you’ve already built scripts that route calls using lists, store customer data in dictionaries, lock in metadata with tuples, and de-dupe entries with sets. That’s solid progress.
But as your logic grows, two pain points start showing up fast:
- Reusability: You end up repeating the same logic, checking a plan type, validating an input, or printing the same output in five places. That’s fragile, messy, and hard to debug.
- Resilience: Your code works fine… until a user enters bad input or an API doesn’t respond. Then it breaks. In real-world workflows, that’s not just annoying, it’s costly.
This chapter gives you the tools to fix both. You’ll learn to:
- Define functions to wrap logic into reusable building blocks.
-
Add parameters and use
returnto pass data in and out cleanly. -
Use
tryandexceptto catch failures and keep your scripts alive,even when things go wrong.
Whether you’re building CX automations, contact flows, or just sharpening your Python basics, this is the part where your scripts start behaving like real systems. Let’s get into it.
Functions: Your Code’s Building Blocks
If your script is getting longer, or if you’re repeating the same chunk of logic across multiple flows, it’s time to start using functions. Think of functions like reusable subroutines in a bot builder or custom actions in a CPaaS flow: you define them once, then call them whenever you need the behavior.
In this section, you'll learn how to define functions, pass them
input data (parameters), get output values (using
return), and structure your logic for reuse. We’ll
do it all using real CX examples from previous parts,
dictionaries, tags, intent routing, and more.
Define a Reusable Function
Let’s say you want to return a different welcome message depending on customer tier. Here’s how you can do that with a function:
def generate_welcome_message(customer_name, customer_tier="guest"):
if customer_tier.lower() == "vip":
return f"Welcome back, {customer_name}! Your dedicated agent is ready to assist."
else:
return f"Hello {customer_name}, how can I help you today?"
print(generate_welcome_message("Alice"))
print(generate_welcome_message("Bob", customer_tier="VIP"))
Hello Alice, how can I help you today?
Welcome back, Bob! Your dedicated agent is ready to assist.
Returning Results Based on Input Data
Now let’s use a customer dictionary like we did in Part 3. This function returns a priority level based on tier and number of recent interactions:
def get_customer_priority(customer_data):
interactions = customer_data.get("interactions_today", 0)
tier = customer_data.get("tier", "standard").lower()
if tier == "platinum" or (tier == "gold" and interactions > 5):
return "High"
elif interactions > 2:
return "Medium"
else:
return "Normal"
customer1 = {"name": "Alice", "tier": "gold", "interactions_today": 7}
customer2 = {"name": "Bob", "tier": "standard", "interactions_today": 1}
customer3 = {"name": "Charlie", "tier": "platinum", "interactions_today": 0}
print(f"{customer1['name']} priority: {get_customer_priority(customer1)}")
print(f"{customer2['name']} priority: {get_customer_priority(customer2)}")
print(f"{customer3['name']} priority: {get_customer_priority(customer3)}")
Functions That Return Values vs. Side Effects
Validate a phone number:
def validate_phone_number(number):
return number.isdigit() and len(number) == 10
print(f"The number is valid: {validate_phone_number('9876543210')}")
Log an event with a timestamp:
from datetime import datetime
def log_event(event):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] EVENT: {event}")
log_event("User logged in")
Notice how validate_phone_number()
returns a result, while
log_event() handles everything
inside the function? That’s intentional.
If you want to retrieve data from a function and decide what
to do with it (print, store, compare), use
return. If the function just performs an action,
like logging or sending a message, you don’t need to return
anything.
from datetime import datetime? That’s
Python pulling in a built-in module, like a
toolbox. The first datetime is the module, the
second is the class inside it. We’ll unpack this more in Part 5.
Summarize a customer profile:
def get_customer_summary(customer_id, customer_database):
customer_data = customer_database.get(customer_id)
if customer_data:
registered_date = customer_data["meta"][1] if "meta" in customer_data and len(customer_data["meta"]) > 1 else "N/A"
return f"Customer ID: {customer_id}\nName: {customer_data['name']}\nStatus: {customer_data['status']}\nTags: {', '.join(customer_data['tags']) if customer_data['tags'] else 'None'}\nRegistered: {registered_date}"
else:
return f"Customer ID {customer_id} not found."
sample_customer_db = {
"C1044": {"name": "Alice", "status": "Active", "tags": {"priority", "billing"}, "meta": ("C1044", "2024-07-01")},
"C1045": {"name": "Bob", "status": "Inactive", "tags": set(), "meta": ("C1045", "2024-06-15")}
}
print(get_customer_summary("C1044", sample_customer_db))
print("\n" + get_customer_summary("C999", sample_customer_db))
-
Build
generate_bot_response(intent, language="en"), use a dictionary of prewritten messages and return the right one. -
Write
clean_input_string(text)that trims spaces and lowercases a string. - Try calling both with different values. What happens when something’s missing?
Error Handling: Making Your Scripts Robust
Real-world automation doesn’t always go as planned, customers mistype inputs, APIs fail, or your logic hits unexpected values. When scripts crash, customer experience suffers. That’s where error handling becomes a non-negotiable.
In Python, the try-except block lets you
catch problems before they crash your script.
Think of it like fallback routing in an IVR or a default agent
assignment when no specialist is available. A backup plan, built
into your logic.
Basic Structure
try:
risky_code()
except SomeError:
handle_the_problem()
If something in the try block fails, Python jumps
to the except block. This lets you keep things
running and provide helpful feedback.
Common Error Types (CX-relevant)
-
ValueError– Invalid input (e.g., converting "abc" toint()) -
KeyError– Accessing a missing key in a dictionary -
IndexError– Trying to access an out-of-range item in a list
Example: Validating Input Duration
try:
duration = int(input("Enter call duration in seconds: "))
print(f"Logged call for {duration} seconds.")
except ValueError:
print("Invalid input. Please enter a number.")
Example: Dictionary Lookup (Customer Profile)
customer = {
"id": "C301",
"name": "Maya",
"tier": "gold"
}
try:
print("Language:", customer["language"])
except KeyError:
print("Language not found in profile.")
Language not found in profile.
customer.get("language", "N/A") if you just want a
safe fallback. Use try-except when you expect
multiple things might go wrong, or when you need to react to a
missing key specifically.
Example: Accessing Call Records (List Index)
recent_calls = ["Call_101", "Call_102"]
try:
print("Call record:", recent_calls[2])
except IndexError:
print("Error: That call record does not exist in the list. Check the index.")
Error: That call record does not exist in the list. Check the index.
Optional: else and finally
You can add an else block that runs only if no
error occurred, and a finally block that runs no
matter what, great for cleanup or logging.
try:
score = int(input("Enter CSAT score: "))
except ValueError:
print("Please enter a number.")
else:
print("Thank you! Score recorded.")
finally:
print("End of CSAT flow.")
Enter CSAT score: 9
Thank you! Score recorded.
End of CSAT flow.
Example: Simulated API Call Failure
def simulate_api_call():
raise ConnectionError("API service not responding")
try:
simulate_api_call()
except ConnectionError as e:
print("Alert: Could not connect to API.")
print("Details:", e)
This example simulates what happens if your script tries to
reach an external service, like a customer data API or sentiment
analysis engine, and it’s down. The function uses
raise to deliberately trigger an error, which we
then catch and handle gracefully.
-
Create a function
process_user_csat_score()that asks the user to enter a CSAT score (1–10). HandleValueErrorfor non-numeric input. Print "Score recorded" on success or a custom error otherwise. -
Use a list
recent_calls = ['XYZ123', 'ABC456']. Ask the user for an index. HandleIndexErrorif the input index doesn’t exist. -
Use
finallyto print"Thanks for using the tool."at the end.
Mini Project – A Resilient Agent Assist CX Tool
Time to put everything together, functions, loops, input validation, dictionaries, error handling, and build something real. This mini project simulates a lightweight agent-assist tool you might use in a contact center environment.
This version is more than a toy example. It’s designed to be modular, fault-tolerant, and CX-aware. You’ll see how the concepts from Part 3 (structured customer data, dictionaries, sets, and tuples) combine with the robustness strategies from Part 4 (error handling, try-except, safe input, simulated API failures).
Here’s what it will do:
- Let agents look up customer info from a structured dictionary
- Log agent-entered queries and handle failed logging attempts
- Show a quick summary of recent queries
- Validate input and handle unexpected errors gracefully
Example Output (Simulated Run)
--- Agent Assist CX Tool ---
1. Fetch Customer Info
2. Log Query
3. Show Log Summary
4. Exit
Choose an option (1–4): 1
Enter customer ID: C102
Customer Name: Bob
Status: Escalated
Tags: billing
Registered On: 2024-06-28
Choose an option (1–4): 2
Enter your query: I need help with my bill
Query logged successfully.
Log attempt complete.
Choose an option (1–4): 2
Enter your query:
Empty query. Nothing logged.
Log attempt complete.
Choose an option (1–4): 3
Total Queries Logged: 1
Recent Queries:
- I need help with my bill
The Code
from datetime import datetime
import random
# Simulated customer database with structured metadata
customer_db = {
"C101": {"name": "Alice", "status": "Active", "tags": {"vip"}, "meta": ("C101", "2024-07-01")},
"C102": {"name": "Bob", "status": "Escalated", "tags": {"billing"}, "meta": ("C102", "2024-06-28")},
"C103": {"name": "Charlie", "status": "Pending", "tags": set(), "meta": ("C103", "2024-07-03")},
}
query_log = []
def get_valid_input(prompt_msg, expected_type=int):
while True:
user_input = input(prompt_msg)
try:
return expected_type(user_input)
except ValueError:
print(f"Invalid input. Please enter a valid {expected_type.__name__}.")
def fetch_customer_profile(cid):
try:
profile = customer_db[cid]
print(f"\nCustomer Name: {profile['name']}")
print(f"Status: {profile['status']}")
print(f"Tags: {', '.join(profile['tags']) if profile['tags'] else 'None'}")
print(f"Registered On: {profile['meta'][1]}")
except KeyError:
print("Customer ID not found.")
def simulate_remote_log(query):
# Randomly simulate API failure (30% chance)
if random.random() < 0.3:
raise ConnectionError("Remote logging service unreachable.")
return True
def log_customer_query(query):
if query.strip() == "":
print("Empty query. Nothing logged.")
return
try:
if simulate_remote_log(query):
query_log.append(query)
print("Query logged successfully.")
except ConnectionError as ce:
print("Could not log query remotely.")
print("Reason:", ce)
print("Saving locally for retry later...")
query_log.append(f"[PENDING SYNC] {query}")
finally:
print("Log attempt complete.\n")
def show_menu():
print("\n--- Agent Assist CX Tool ---")
print("1. Fetch Customer Info")
print("2. Log Query")
print("3. Show Log Summary")
print("4. Exit")
def summarize_queries():
print(f"\nTotal Queries Logged: {len(query_log)}")
print("Recent Queries:")
for q in query_log[-3:]:
print("-", q)
# Main interaction loop
while True:
show_menu()
choice = input("Choose an option (1–4): ").strip()
if choice == "1":
cid = input("Enter customer ID: ").strip()
fetch_customer_profile(cid)
elif choice == "2":
q = input("Enter your query: ").strip()
log_customer_query(q)
elif choice == "3":
summarize_queries()
elif choice == "4":
print("Goodbye! Stay resilient.")
break
else:
print("Invalid option. Try again.")
log_customer_query() encapsulate error handling,
retries, and logging logic. You can update that function in the
future to write to a real database or external system, without
touching the rest of your script.
Ready to Extend It?
-
Add a function to
tagcustomers as VIP or for follow-up - Track which agent submitted each query (using input and attribution)
- Export query logs to a file (coming in Part 5!)
- Let users browse logs by customer ID or tag filter
summarize_queries() that displays the total number
of queries logged and shows the last 3 entries. Already built in
this version, but can you tweak it to show only VIP customer
queries?
Other Concepts You’ll Start Noticing
As your scripts grow more modular and resilient, you’ll start to notice patterns that aren’t strictly “basic,” but show up again and again, especially in production-grade CX automation. You don’t need to master these yet, but it helps to recognize them when they appear.
Here’s what these concepts look like in real CX workflows:
| Concept | Description & Example |
|---|---|
param=default |
Default Parameters: Functions can provide
fallback values when input is missing, just like bots that
default to English when language isn’t set.
|
| Scope |
Scope: Variables created inside a
function are isolated from your main script. This protects
you from accidental changes.
You can read global variables inside a function, but
modifying them requires the |
pass |
pass: Use this as a
placeholder when planning future features. Keeps your
structure clean while you build incrementally.
|
| Docstrings |
Docstrings: These are multi-line comments
that explain what your function does. They’re helpful for
teammates, and your future self.
You can view this documentation using:
|
-
Take your
send_welcome()orgenerate_message()function and:- Add a docstring describing its purpose, parameters, and output
-
Add a default parameter like
customer_segment="standard"and customize the message if they’re"vip"
-
Create a stub for a new function called
initiate_escalation(customer_id, reason)usingpass. Add a docstring explaining what it’s meant to do, even if you haven’t implemented it yet.
Gotchas and How to Avoid Them
-
Forgetting
returnin a function: If your function doesn’t explicitly return a value, it returnsNoneby default. If you try to use that result in an operation (e.g., math or string join), you’ll hit aTypeErrorand your script may crash.
Always double-check: are you calling a function for its side effect or expecting a result back? -
Catching overly broad exceptions (
except Exception:): Avoid this unless absolutely necessary. It masks real bugs, swallows important error messages, and makes debugging a nightmare.
Be specific: CatchValueError,KeyError, etc., based on what you expect could go wrong. -
Modifying global variables inside functions:
Reassigning global variables requires the
globalkeyword, which is risky and discouraged. Instead, return new values and assign them outside the function. -
Hidden Side Effects from Global Lists &
Dictionaries:
Here’s a subtle one: you don’t need
globalto modify a global list or dictionary. This can silently mutate global state.
Be careful when appending to a global log, updating shared data, or modifying customer state inside a function.query_log = [] def log_query(q): query_log.append(q) # Modifies the global list log_query("System error") print(query_log) # ['System error'] -
Not catching all relevant exceptions:
Some operations might fail in multiple ways, e.g., input
conversion can raise
ValueError, and dictionary lookups can raiseKeyError.
Use multipleexceptblocks or a tuple of exceptions if appropriate:try: # simulate API + input score = int(input("Score? ")) data = customer_profile["language"] except (ValueError, KeyError) as e: print("Something went wrong:", e)
These “gotchas” aren’t just theory, they’re the exact kind of pitfalls that show up in customer automation, reporting scripts, or agent tools that run for hours unattended.
When a chatbot logs garbage input, or your function returns
None and breaks the billing flow, it's not just a
learning moment, it’s a ticket. A real one.By learning to spot these issues early, and fixing them with intent, you’re building not just Python scripts, but production-grade automation skills. That’s how pros ship tools that don’t embarrass you at 3 AM.
Up Next: Modules & File I/O
You’ve just taken a big step forward moving from one-off scripts
to reusable, resilient code. You’ve learned how to write
functions that return results, handle errors
gracefully with try-except, and build cleaner CX
workflows using real logic, not just print statements.
In Part 5, we’ll go one step further: making your Python programs truly interactive with the outside world. That means reading and writing files, organizing code into modules, and preparing your scripts for real deployment.
- File I/O: Save chat logs, pull config from flat files, and persist data, all with just a few lines of Python.
- Custom modules: Split up large scripts into reusable components across files. Build like the pros do.
- Real-world workflows: Store fallback prompts, version FAQs, read from CSVs, everything your automation flows need.
Part 5 is where your code stops living in the editor and starts behaving like a real tool, with memory, structure, and scale.
See you in Part 5, where we go from resilient scripts to integration-ready systems.
A Note Before You Go
You’ve come a long way. From basic commands to building modular, resilient scripts, this post wasn’t just about Python syntax. It was about writing code with intent. Code that solves real problems. Code that belongs in the tools you use every day.
Whether you’re building chatbots, contact center flows, automation scripts, or just sharpening your skills, I hope this post helped connect the dots between fundamentals and functional, real-world logic.
There are thousands of Python tutorials out there. But I wanted this one to feel different: practical examples, CX-themed use cases, and a mindset that grows with you as your projects scale.
No tutorial will make you great. But every small project, every fix, every experiment brings you one step closer. So keep building. Test often. Break things. Improve. That’s how you get from playground demos to production-grade code.
If you're ready to level up, here are some great places to sharpen your skills: