AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Building a Chatbot with Python

📚 Natural Language Processing⏱️ 25 min read🎓 Grade 9
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 25 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

Open any Indian Railways app and try typing "Where is my train?" into the help chat. It answers instantly. It does not truly know what a train is, and it has never travelled anywhere. What it has is a set of rules written by a programmer, and a fast way of checking your sentence against those rules. In this chapter you will build exactly that kind of chatbot in Python, watch it fail in a specific and instructive way, and then fix it properly. By the end you will understand not just how to write a chatbot, but why the "obvious" way of writing one is quietly broken, and how real conversational AI is a different animal altogether.

1. What a Chatbot Actually Does

Strip away the word "AI" for a moment. A chatbot, at the simplest level, is a function. It takes one input — a line of text you typed — and produces one output — a line of text it prints back. That's it. The intelligence, such as it is, lives entirely in how the function decides what to output. In this chapter we will write that function ourselves, several times, each version fixing a flaw in the previous one.

The simplest possible strategy is pattern matching: look at what the user typed, compare it against a list of known patterns, and reply with a fixed response tied to whichever pattern matched. This is precisely the strategy behind most "customer support bots" you meet on shopping or ticket-booking websites before you get escalated to a human. It is not intelligent in any deep sense, but it is genuinely useful, and understanding it well is the right foundation before you ever meet a large language model.

2. Version 1 — Matching Exact Words

Let's write the smallest chatbot that could possibly work. It checks if the user's message is exactly equal to a word we expect, using Python's if-elif-else structure.

def chatbot_v1(user_input):
    if user_input == "hi":
        return "Hello! How can I help you?"
    elif user_input == "bye":
        return "Goodbye! Have a nice day."
    else:
        return "I don't understand."

print(chatbot_v1("hi"))
print(chatbot_v1("Hi"))

Trace this line by line, the way you should trace every program before trusting it. chatbot_v1("hi") runs the function with user_input bound to the string "hi". Python checks user_input == "hi": the strings are identical, so the condition is True, and the function returns "Hello! How can I help you?". The first print shows that line.

Now the second call: chatbot_v1("Hi"). Here user_input is "Hi", with a capital H. Python string comparison is case-sensitive — the character 'H' (uppercase) and 'h' (lowercase) are different values internally, so "Hi" == "hi" evaluates to False. The elif checks "Hi" == "bye", also False. So control falls to else, and the function returns "I don't understand." — even though any human reader can see the user was clearly saying hello.

This is a real bug, not a hypothetical one. It is the single most common mistake beginners make when writing their first pattern-matching program: forgetting that a computer treats "Hi", "hi", and "HI" as three completely different pieces of text unless you explicitly tell it otherwise.

3. Fixing Case Sensitivity

The fix is a single method call. Python strings have a built-in method .lower() that returns a new string with every uppercase letter converted to lowercase. We apply it once, right at the start, and compare against that normalised version instead of the raw input.

def chatbot_v2(user_input):
    text = user_input.lower()
    if text == "hi":
        return "Hello! How can I help you?"
    elif text == "bye":
        return "Goodbye! Have a nice day."
    else:
        return "I don't understand."

print(chatbot_v2("HI"))
print(chatbot_v2("Hi"))

Trace chatbot_v2("HI"): user_input is "HI". The line text = user_input.lower() creates a new string "hi" and stores it in text — note that .lower() does not modify user_input itself; strings in Python cannot be changed in place, so it hands back a fresh string. Now text == "hi" compares "hi" with "hi": True. The function returns the greeting. Both calls now correctly produce "Hello! How can I help you?". One line of code, .lower(), has quietly tripled the number of inputs our bot handles correctly, because it collapses "hi", "Hi", "HI", and "hI" into a single comparable form. This idea — normalising input before comparing it — is one of the most important habits in text processing, and you will meet it again and again in any real NLP system.

4. Being More Flexible — and Walking Into a Trap

Version 2 still has a serious limitation: it only responds correctly if the user types exactly "hi" or "bye" and nothing else. Real users type "hi there", "hii", "say bye now". An exact == comparison fails on every one of these, because the full string is not identical to the target word.

The natural next idea is to stop asking "is the whole message equal to hi?" and instead ask "does the message contain hi anywhere?" Python's in operator does exactly this for strings — it checks whether one string occurs as a contiguous run of characters inside another.

def chatbot_v3(user_input):
    text = user_input.lower()
    if "hi" in text:
        return "Hello! How can I help you?"
    elif "bye" in text:
        return "Goodbye! Have a nice day."
    else:
        return "I don't understand."

print(chatbot_v3("hi there"))
print(chatbot_v3("I bought a shirt yesterday"))

The first call looks like a clean win. text = "hi there", and "hi" in "hi there" checks whether the two characters h then i appear consecutively anywhere in the longer string. They do, right at the start, so the condition is True and we get the greeting — an improvement over Version 2, which would have failed on this input.

Now trace the second call carefully, character by character, because this is where the design goes wrong. text = "i bought a shirt yesterday" (already lowercase). Python checks "hi" in text. This does not ask "is 'hi' a separate word in this sentence?" — it asks "do the characters h, i appear next to each other anywhere in this string at all?" Look at the word shirt: s‑h‑i‑r‑t. The second and third letters are exactly h then i. So the substring "hi" genuinely does occur inside "shirt", at that position, and "hi" in text evaluates to True. The condition matches, and chatbot_v3 confidently replies "Hello! How can I help you?" to a sentence that was not a greeting at all — someone talking about clothing they bought.

Common misconception, stated plainly and corrected: many students assume the in operator on strings checks whether a word is present. It does not. It checks whether a sequence of characters is present, with no regard for word boundaries, spaces, or meaning. "hi" hides inside "shirt", "chip", "whisper", and "achieve" for exactly this reason — each of those words happens to contain the letters h and i next to each other. This is not a rare edge case; any keyword-matching program built carelessly with in on raw strings will eventually misfire on ordinary vocabulary, and the failures are silent — the program runs without crashing, it just answers the wrong question.

5. The Real Fix — Matching Whole Words, Not Fragments

The bug lives in comparing against the raw sentence. The fix is to first break the sentence into its individual words, and then check whether "hi" is one of those words, not whether it appears as a fragment anywhere. Python's .split() string method does exactly the breaking-up step: called with no arguments, it splits a string wherever there is one or more spaces, and returns a list of the pieces.

def chatbot_v4(user_input):
    words = user_input.lower().split()
    if "hi" in words:
        return "Hello! How can I help you?"
    elif "bye" in words:
        return "Goodbye! Have a nice day."
    else:
        return "I don't understand."

print(chatbot_v4("I bought a shirt yesterday"))
print(chatbot_v4("hi there"))

Trace the first call. user_input.lower() gives "i bought a shirt yesterday". Calling .split() on that turns it into the list ["i", "bought", "a", "shirt", "yesterday"] — five separate string elements, split at each space. Now the check is "hi" in words. This time in is operating on a list, not a string, and list membership works differently: Python asks "is the exact string 'hi' equal to any single element of this list?" It compares "hi" against "i" (no), "bought" (no), "a" (no), "shirt" (no — the whole element is "shirt", not "hi", and list membership needs full equality, not partial overlap), "yesterday" (no). None match, so the condition is False. The elif also fails, and the function correctly falls through to "I don't understand." — the bug from Version 3 is gone.

Trace the second call to confirm the fix didn't break the case that should work. "hi there".split() gives ["hi", "there"]. "hi" in ["hi", "there"] compares "hi" against each element: the first element is exactly "hi", full match, condition True. The greeting is returned, correctly.

This is the core lesson of the chapter: the same keyword-matching idea can be implemented as a string-substring check or a word-list check, and only one of them is safe. Whenever you write a program that looks for a keyword inside user text, tokenising into words first — and comparing whole tokens — is what separates a bot that works from one that quietly misfires on ordinary sentences.

6. Scaling Up — a Dictionary of Intents

A chain of if-elif statements becomes unmanageable once you want to handle a dozen kinds of questions. A cleaner structure uses a Python dictionary: think of it like a contacts list on your phone, where instead of looking someone up by scrolling, you look them up directly by name. A dictionary maps a key (like a contact's name) to a value (their number) with no scanning required.

Here we use two dictionaries: one mapping each intent (the category of thing the user wants — a greeting, a status check, thanks, a goodbye) to the list of keywords that signal it, and a second mapping each intent to the possible replies.

keywords = {
    "greet": ["hi", "hello", "hey"],
    "pnr": ["pnr", "ticket", "status"],
    "thanks": ["thanks", "thank"],
    "bye": ["bye", "goodbye"]
}

responses = {
    "greet": ["Hello! I am RailBot. Ask me about your PNR status.",
               "Hi there! How can I help you today?"],
    "pnr": ["Please share your 10-digit PNR number to check status.",
             "I can check PNR status. What is your PNR number?"],
    "thanks": ["You're welcome!", "Happy to help!"],
    "bye": ["Goodbye! Have a safe journey.", "See you soon!"]
}

Now we write one function that finds which intent matches, using the same safe word-membership check from Version 4, but looping over every intent instead of hardcoding four elif branches:

def get_intent(user_input):
    words = user_input.lower().split()
    for intent, key_list in keywords.items():
        for word in words:
            if word in key_list:
                return intent
    return None

keywords.items() gives pairs of (intent name, its keyword list) — for example the pair ("greet", ["hi", "hello", "hey"]). The outer for loop walks through the four intents in the order they were written: "greet", "pnr", "thanks", "bye". For each intent, the inner loop checks every word in the user's sentence against that intent's keyword list, and returns immediately the moment any word matches. If no intent ever matches, the function falls through both loops and returns None, Python's built-in "nothing here" value.

Trace get_intent("hi can you check my ticket status"). words = ["hi", "can", "you", "check", "my", "ticket", "status"]. The outer loop starts with intent "greet", key list ["hi", "hello", "hey"]. The inner loop checks each word in order: the very first word is "hi", and "hi" in ["hi", "hello", "hey"] is True immediately. The function returns "greet" right there, without ever looking at "ticket" or "status". This is worth noticing as a limitation, not a bonus: the sentence is really asking about a ticket, but because it happens to also contain "hi", and greet is checked first, the bot classifies it as a greeting. Rule-based bots like this one commit to the first matching category they find; they do not weigh which intent the sentence is "mostly" about.

7. A Numeric Fix — Scoring Instead of Stopping at the First Match

We can do better with simple counting. Instead of returning the first intent that matches even one keyword, count how many keywords from each intent appear in the sentence, and pick the intent with the highest count. This turns intent detection into simple arithmetic.

def get_intent_scored(user_input):
    words = user_input.lower().split()
    scores = {}
    for intent, key_list in keywords.items():
        score = 0
        for word in words:
            if word in key_list:
                score = score + 1
        scores[intent] = score
    best_intent = max(scores, key=scores.get)
    if scores[best_intent] == 0:
        return None
    return best_intent

Trace it on the same sentence, "hi can you check my ticket status", adding "pnr" to make the numbers more interesting: "hi my ticket pnr status please". words = ["hi", "my", "ticket", "pnr", "status", "please"].

For intent "greet", key list ["hi", "hello", "hey"]: scanning the six words, only "hi" matches, so score = 1. scores["greet"] = 1.

For intent "pnr", key list ["pnr", "ticket", "status"]: "ticket" matches (score becomes 1), "pnr" matches (score becomes 2), "status" matches (score becomes 3). scores["pnr"] = 3.

For "thanks" and "bye", no words match either key list, so both score 0. The full scoreboard is {"greet": 1, "pnr": 3, "thanks": 0, "bye": 0}. max(scores, key=scores.get) looks at each key's associated value and returns the key with the largest one — here, "pnr", with a score of 3, clearly wins over greet's score of 1. The function correctly identifies that this sentence is really about checking a PNR, not a greeting, precisely because it counts evidence instead of stopping at the first hit. This small piece of arithmetic — tally the matches, pick the largest tally — is the same basic idea, vastly scaled up, behind how real search engines rank which of your keywords a webpage matches best.

8. Adding Variety and Running a Live Conversation

A bot that gives the identical sentence every single time feels robotic in the worst way. The random module's random.choice(a_list) function picks one element from a list at random, so each intent can have two or three possible replies instead of one fixed line.

import random

print("RailBot: Hello! Type 'bye' to exit.")
while True:
    user_input = input("You: ")
    intent = get_intent_scored(user_input)
    if intent == "bye":
        print("RailBot:", random.choice(responses["bye"]))
        break
    elif intent is None:
        print("RailBot: Sorry, I did not understand. Try asking about PNR status.")
    else:
        print("RailBot:", random.choice(responses[intent]))

The while True: loop runs forever until something inside it explicitly executes break. Each pass asks the user for a line with input(), classifies it with the scored intent function you already traced above, and prints a randomly chosen reply from the matching list — or the fallback message if intent came back None. Typing anything containing "bye" as its highest-scoring intent ends the loop. This is a complete, runnable command-line chatbot built from ideas you can trace by hand: string normalisation, list membership, dictionaries, loops, and one line of arithmetic.

9. Where This Approach Stops Working

It is worth being precise about what this chatbot can and cannot do, because the gap is exactly where modern AI systems like ChatGPT differ from what you just built. This chatbot has no concept of grammar, word order, negation, or context between messages — it would score "status" the same way in "what is my PNR status" and in "my PNR status is not needed, thanks", because it only counts keyword matches, blind to the word "not" changing the meaning. It cannot handle a keyword it has never been told about; ask it about a "refund" and, unless you add "refund" to some intent's keyword list yourself, it has no way to respond sensibly, however common that question might be in real life.

Large language models take a fundamentally different approach: rather than matching your sentence against a small hand-written list of keywords, they are trained on enormous amounts of text to estimate, for any sequence of words, which word is statistically likely to come next — repeated word by word to generate a full reply. They do not "look up" an intent in a dictionary the way get_intent_scored does; there is no fixed list of categories at all. That statistical approach lets them respond sensibly to sentences they have never seen verbatim, including ones with negation and complex grammar — but it is a genuinely different mechanism from the rule-based matching in this chapter, not a bigger version of the same dictionary lookup. Both approaches are legitimate engineering; a rule-based bot is faster to build, perfectly predictable, and often good enough for a narrow task like checking PNR status, which is exactly why many real customer-support bots on Indian websites still use pattern matching much like get_intent_scored rather than a full language model.

10. Recap of the Core Misconception

If you remember only one thing from this chapter, make it this: "hi" in some_string checks for two consecutive characters anywhere at all, while "hi" in some_list_of_words checks for an exact whole-word match. The first quietly fires inside words like "shirt", "chip", "whisper", and "achieve" — none of which are greetings — because each one happens to contain the letters h and i side by side. The second does not, because "shirt" as a complete list element is never equal to the string "hi", no matter what letters it contains. Every keyword-matching chatbot you write from now on should tokenise with .split() and check word membership, not raw substring presence, unless you have a specific reason to want partial matches.

Diagram: Substring Match vs. Word Match

Substring matching versus word-token matching for the keyword "hi" The sentence "I bought a shirt yesterday" wrongly matches "hi" as a substring check because "shirt" contains h-i, but correctly fails to match when checked as a list of whole words. Same keyword check, two different results Substring check: "hi" in text i bought a shirt yesterday "hi" in text -> True (WRONG) matched inside "shirt", not a greeting Token check: "hi" in words i bought a shirt yesterday each box is one full word; "hi" must equal a whole box to match "hi" in words -> False (correct) Input: "hi there" "hi" in text -> True "hi" in words -> True Both checks agree here -- "hi" really is a whole word, so both correctly greet. The token check is the one to trust always -- it is never wrong the way substring checks silently can be.

Practice — Test Yourself Before Moving On

  1. Trace get_intent("thank you for the whisper of good news") using the word-list version from Section 6. List the words produced by .split(), and state clearly which intent is returned and why "whisper" does not cause a problem here even though it contains "hi".
  2. Write, by hand on paper, what "bye" in "goodbye everyone".split() evaluates to, and explain your reasoning in one sentence. (Careful: .split() here breaks on spaces only — check whether "bye" is ever a complete element of the resulting list.)
  3. Using the scoring idea from Section 7, compute by hand the score dictionary for the sentence "hello, thanks for checking my ticket and pnr status" against the keywords dictionary given in Section 6, ignoring punctuation attached to "hello,". Which intent wins, and by how much?
  4. Add a new intent called "help" to both the keywords and responses dictionaries, with keywords like "help" and "support", and at least two possible responses. Trace what get_intent_scored("I need help with my status") returns after your change.
  5. Explain in two or three sentences why a chatbot built entirely this way — from dictionaries of keywords — could never correctly handle the sentence "I do not want to cancel my ticket", if "cancel" were mistakenly added as a keyword for an intent that cancels tickets.

Summary

A rule-based chatbot is, underneath the surface, just a function from text to text, built out of tools you already know: if/elif to branch, .lower() to normalise case, and comparison to decide what to reply. Making it flexible with the in operator introduces a specific, well-defined bug: substring checks on raw strings fire inside unrelated words like "shirt", "chip", or "achieve" whenever those words happen to contain the target letters in sequence. Splitting the input into a list of words with .split() and checking whole-word membership instead — "hi" in words rather than "hi" in text — removes that bug entirely, because list membership requires an element to be fully equal to the target, not merely to contain it. Scaling from a few elif branches to a dictionary of intents and keywords makes the bot easier to extend, and counting keyword matches per intent (simple arithmetic) lets it choose the best-supported intent instead of freezing on the first keyword it happens to see. All of this remains fundamentally different from how large language models generate replies, which predict likely next words from patterns learned over huge amounts of text rather than matching against a hand-written keyword list — a distinction worth holding onto as you meet more sophisticated NLP tools later in this course.

Think About It

Think about this: How would you explain building a chatbot with python to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.

← Time Series Analysis with PythonParallel Computing: Making Programs Faster →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn