A simple Python chatbot is an excellent beginner project because it combines input, strings, conditions, loops, functions, dictionaries, files, and testing in one practical application. The bot in this guide runs in the terminal and responds through explicit rules. It does not require a web service or a machine-learning model.
Rule-based chatbots are useful when the possible questions are limited and predictable, such as a help menu, study assistant, product information tool, or frequently asked questions interface. They also provide a clear foundation for understanding more advanced conversational systems.
How a rule-based chatbot works
The program follows a repeated process:
- Read a message from the user.
- Normalize the text.
- Identify a matching intent or keyword.
- Select a suitable reply.
- Display the reply.
- Continue until the user asks to leave.
This is an algorithm made of input, processing, output, and repetition. The concepts are covered in the programming logic guide and the article explaining algorithms in programming.
Create the project
Create a folder and a file named chatbot.py. A virtual environment is optional for this first version because it uses only the standard library, but an environment becomes useful when external packages are added. See the Python venv guide for setup instructions.
Build the smallest conversation loop
print("Bot: Hello! Type 'bye' to finish.")
while True:
message = input("You: ")
if message.lower() == "bye":
print("Bot: Goodbye!")
break
print("Bot: I am still learning.")The while True loop continues until break runs. The broader Python loops guide explains loop conditions, break, and continue.
Normalize user input
Users may type different capitalization and extra spaces. Normalize the text before matching it:
def normalize(text: str) -> str:
return " ".join(text.strip().lower().split())This function removes leading and trailing spaces, converts text to lowercase, and replaces repeated whitespace with single spaces.
print(normalize(" HELLO BOT ")) # hello botString operations are fundamental to conversational programs. Our Python slicing guide and related string material provide additional manipulation patterns.
Add exact response rules
def get_response(message: str) -> str:
normalized = normalize(message)
if normalized in {"hello", "hi", "hey"}:
return "Hello! How can I help?"
if normalized == "how are you":
return "I am running normally. Thanks for asking!"
if normalized in {"bye", "goodbye", "exit"}:
return "Goodbye!"
return "I did not understand that message."A set is useful for groups of equivalent phrases because membership checks are clear and efficient. Learn more in the Python sets guide.
Connect the function to the loop
EXIT_MESSAGES = {"bye", "goodbye", "exit"}
def main() -> None:
print("Bot: Hello! Type 'bye' to finish.")
while True:
message = input("You: ")
normalized = normalize(message)
response = get_response(message)
print(f"Bot: {response}")
if normalized in EXIT_MESSAGES:
break
if __name__ == "__main__":
main()The if __name__ == "__main__" guard lets another file import the functions without immediately starting an interactive conversation.
Match keywords instead of full sentences
Exact matching becomes restrictive when users write longer phrases. Add keyword-based rules:
def get_response(message: str) -> str:
normalized = normalize(message)
if normalized in {"hello", "hi", "hey"}:
return "Hello! How can I help?"
if "hours" in normalized or "open" in normalized:
return "We are available Monday through Friday, 9 AM to 6 PM."
if "price" in normalized or "cost" in normalized:
return "Tell me which product you want to know about."
if "course" in normalized:
return "We offer beginner and intermediate programming courses."
if normalized in EXIT_MESSAGES:
return "Goodbye!"
return "I did not understand. Try asking about hours, prices, or courses."Keyword matching is easy to understand but can produce false matches. The word “open” may refer to opening a file rather than business hours. As rules grow, organize them by intent and use more precise conditions.
Represent intents with dictionaries
INTENTS = {
"greeting": {
"keywords": {"hello", "hi", "hey"},
"responses": [
"Hello! How can I help?",
"Hi! What would you like to know?",
],
},
"hours": {
"keywords": {"hours", "open", "closing"},
"responses": [
"We are available Monday through Friday, 9 AM to 6 PM."
],
},
"pricing": {
"keywords": {"price", "cost", "pricing"},
"responses": [
"Tell me which product or plan you want to check."
],
},
}Dictionaries group related values under meaningful keys. The Python collections comparison explains when to use dictionaries, lists, sets, and tuples.
Detect an intent
def detect_intent(message: str) -> str | None:
words = set(normalize(message).split())
for intent_name, intent_data in INTENTS.items():
if words & intent_data["keywords"]:
return intent_name
return NoneThe set intersection operator & returns words present in both sets. If the intersection is not empty, at least one keyword matched.
Select varied replies
import random
def get_response(message: str) -> str:
normalized = normalize(message)
if normalized in EXIT_MESSAGES:
return "Goodbye!"
intent = detect_intent(message)
if intent is None:
return "I did not understand. Try asking about hours or prices."
responses = INTENTS[intent]["responses"]
return random.choice(responses)Random alternatives make repeated conversations less mechanical. Do not use randomness for critical information such as prices, legal notices, or account status unless every alternative is equivalent.
Load intents from JSON
Separating content from code allows non-programmers to edit responses. Create intents.json:
{
"greeting": {
"keywords": ["hello", "hi", "hey"],
"responses": [
"Hello! How can I help?",
"Hi! What would you like to know?"
]
},
"hours": {
"keywords": ["hours", "open", "closing"],
"responses": [
"We are available Monday through Friday, 9 AM to 6 PM."
]
}
}Load and validate it:
import json
from pathlib import Path
def load_intents(path: str | Path) -> dict:
file_path = Path(path)
with file_path.open("r", encoding="utf-8") as file:
data = json.load(file)
if not isinstance(data, dict):
raise ValueError("intent data must be a JSON object")
for name, intent in data.items():
if "keywords" not in intent or "responses" not in intent:
raise ValueError(f"invalid intent: {name}")
intent["keywords"] = set(intent["keywords"])
return dataThe official Python JSON documentation describes encoding and decoding. The pathlib guide explains file paths and directory handling.
Create a complete version
import json
import random
from pathlib import Path
EXIT_MESSAGES = {"bye", "goodbye", "exit"}
def normalize(text: str) -> str:
return " ".join(text.strip().lower().split())
def load_intents(path: str | Path) -> dict:
with Path(path).open("r", encoding="utf-8") as file:
data = json.load(file)
for name, intent in data.items():
if not intent.get("keywords") or not intent.get("responses"):
raise ValueError(f"intent {name!r} is incomplete")
intent["keywords"] = set(intent["keywords"])
return data
def detect_intent(message: str, intents: dict) -> str | None:
words = set(normalize(message).split())
for intent_name, intent_data in intents.items():
if words & intent_data["keywords"]:
return intent_name
return None
def get_response(message: str, intents: dict) -> str:
normalized = normalize(message)
if normalized in EXIT_MESSAGES:
return "Goodbye!"
intent = detect_intent(message, intents)
if intent is None:
return "I did not understand. Please try another question."
return random.choice(intents[intent]["responses"])
def main() -> None:
intents = load_intents("intents.json")
print("Bot: Hello! Type 'bye' to finish.")
while True:
message = input("You: ")
response = get_response(message, intents)
print(f"Bot: {response}")
if normalize(message) in EXIT_MESSAGES:
break
if __name__ == "__main__":
main()Handle file and JSON errors
A friendly command-line program can report expected configuration problems:
def main() -> None:
try:
intents = load_intents("intents.json")
except FileNotFoundError:
print("Bot configuration file was not found.")
return
except (json.JSONDecodeError, ValueError) as error:
print(f"Invalid bot configuration: {error}")
return
# Start the conversation here.Catch specific exceptions and preserve enough information to correct the problem. Our try and except guide explains this practice.
Remember a user’s name
Add a small state dictionary:
def main() -> None:
intents = load_intents("intents.json")
state = {"name": None}
while True:
message = input("You: ")
normalized = normalize(message)
if normalized.startswith("my name is "):
state["name"] = message.strip()[11:].strip().title()
print(f"Bot: Nice to meet you, {state['name']}!")
continue
response = get_response(message, intents)
if state["name"] and normalized in {"hello", "hi", "hey"}:
response = f"Hello, {state['name']}!"
print(f"Bot: {response}")
if normalized in EXIT_MESSAGES:
breakThis state exists only while the program runs. Durable memory requires a file or database and raises privacy questions about what should be stored and for how long.
Test the chatbot logic
Keep input/output outside the core functions so they can be tested:
def test_normalize_removes_extra_spaces():
assert normalize(" HELLO BOT ") == "hello bot"
def test_exit_message_returns_goodbye():
assert get_response("bye", {}) == "Goodbye!"
def test_detect_intent_matches_keyword():
intents = {
"hours": {
"keywords": {"hours", "open"},
"responses": ["Open weekdays"],
}
}
assert detect_intent("What are your hours?", intents) == "hours"Use Pytest to automate these checks. For deterministic tests, avoid asserting one particular random response; instead, verify that the result belongs to the allowed response list.
Improve matching carefully
Possible extensions include:
- remove punctuation before tokenization;
- assign priority to specific intents;
- match multiword phrases before individual words;
- use regular expressions for structured values such as order numbers;
- add a confidence score based on the number of matching keywords;
- ask a clarification question when two intents tie.
Each improvement should have tests. A more complex matching rule is not useful if it creates unpredictable replies.
Turn the chatbot into another interface
The functions in this guide are independent of the terminal. The same response engine can later power:
- a web page built with Flask or Django;
- a desktop interface;
- a REST API;
- a Telegram bot;
- a help widget inside another application.
Keep interface-specific code separate from intent detection and responses. This separation reduces duplication.
Limitations of a rule-based chatbot
A keyword bot does not truly understand language. It can miss synonyms, context, spelling variations, negation, and references to earlier messages. It also needs manual updates when topics change. These limitations are acceptable when the scope is narrow and responses must remain predictable.
For customer-facing use, clearly disclose that the user is interacting with an automated system and provide a path to human support when the bot cannot solve the issue.
Common beginner mistakes
- One enormous chain of conditions: separate normalization, detection, and response selection into functions.
- No fallback response: every unmatched message needs a safe answer.
- Case-sensitive matching: normalize input first.
- Using substring checks for very short words: token sets reduce accidental matches.
- Mixing content with code: JSON or another structured format makes responses easier to maintain.
- No exit rule: provide an obvious way to end the conversation.
- No tests: a new intent can accidentally capture messages meant for another rule.
Conclusion
A simple Python chatbot brings together core programming skills in a project that produces immediate feedback. Start with a conversation loop and exact rules, then add normalization, intent dictionaries, random alternatives, JSON configuration, state, and tests. Keep the response engine separate from the interface so the same logic can later run in a web app, messaging bot, or desktop program.






