8 ways to check if a Python string contains a substring

Checking whether a Python string contains a substring is one of the first things new developers reach for, whether it’s validating an email address, searching log output or filtering a list of names. Python gives you several ways to do this, from the simple in operator to regular expressions and pandas. This guide covers every method, when to reach for each one, and how to extend the same check across an entire dataset.

Ways to check if a Python string contains a substring

Python does not have a single dedicated contains function on strings. Instead, it gives you a handful of operators and methods, each suited to a slightly different situation.

  • in: returns True or False, the fastest option for a plain existence check
  • not in: the opposite of in, returns True when the substring is missing
  • find(): returns the index of the first match, or -1 if the substring is not found
  • index(): returns the index of the first match, raises ValueError if the substring is not found
  • operator.contains(): a function form of in, useful inside filter() or map()
  • re.search(): matches patterns, including case-insensitive and conditional searches

The sections below walk through each one with syntax and working examples, starting with the option most developers reach for first.

Using the in operator

The in operator is the standard way to check if a Python string contains a substring. It reads almost like plain English and returns a boolean, so it drops directly into an if statement without extra logic.

Syntax:

substring in string

Example:

full_string = "Codeforgeek publishes Python tutorials"
sub_string = "Python"

if sub_string in full_string:
    print("Found the substring!")
else:
    print("Substring not present in the string!")

Output:

Found the substring!

The check is case-sensitive. Searching for “python” in lowercase against the string above returns False, since the capital P does not match. Combine two in checks with the and keyword from Python logical operators when a value needs to contain more than one substring at once, for example checking that a filename contains both a project name and a file extension.

Using the not in operator

Sometimes the useful check is the absence of a substring rather than its presence. The not in operator flips the in check and returns True only when the substring is missing.

full_string = "Codeforgeek publishes Python tutorials"

if "JavaScript" not in full_string:
    print("JavaScript is not mentioned")

Output:

JavaScript is not mentioned

This reads more naturally than wrapping an in check in a not, and it avoids an extra set of parentheses in longer conditions.

Using the find() method

The find() method searches a string for a substring and returns the starting index of the first match. If the substring does not appear anywhere, it returns -1 instead of raising an error, which makes it a safe choice when a missing substring is a normal outcome rather than a bug.

Syntax:

string.find(substring, start, end)
  • substring: the text to search for
  • start (optional): the index to begin searching from
  • end (optional): the index to stop searching at

Example:

full_string = "Codeforgeek publishes Python tutorials"
sub_string = "Python"

position = full_string.find(sub_string)
if position != -1:
    print(f"Found at index {position}")
else:
    print("Not found")

Output:

Found at index 23

find() is also case-sensitive by default. Pair it with lower() on both sides of the check when the casing of the input cannot be guaranteed.

Using the index() method

The index() method behaves exactly like find() when a match exists, returning the position of the first occurrence. The difference shows up when there is no match at all: index() raises a ValueError instead of returning -1, so it needs a try and except block to avoid crashing the program.

Syntax:

string.index(substring, start, end)

Example:

full_string = "Codeforgeek publishes Python tutorials"
sub_string = "Python"

try:
    position = full_string.index(sub_string)
    print(f"Found at index {position}")
except ValueError:
    print("Not found")

Output:

Found at index 23

Use index() only when a missing substring genuinely counts as an exceptional case worth catching. For a routine existence check, find() or in are simpler and skip the try and except block entirely.

Using operator.contains()

The operator module ships a contains() function that does exactly what the in operator does, but as a callable function rather than an operator. This matters when a containment check needs to be passed around as a value, for example inside filter() or as a callback.

Syntax:

operator.contains(string, substring)

Example:

import operator

full_string = "Codeforgeek publishes Python tutorials"
sub_string = "Python"

print(operator.contains(full_string, sub_string))

Output:

True

For a plain if statement, in reads better and most style guides recommend it. Reach for operator.contains() only in the functional-programming cases where a bare operator will not fit the syntax.

Using regular expressions with re.search()

The re module’s search() function checks a string against a pattern rather than an exact substring, which matters once the check needs conditions beyond a plain literal match, such as ignoring case or matching one of several possible words.

Syntax:

re.search(pattern, string, flags)

Example:

import re

full_string = "Codeforgeek publishes Python tutorials"
sub_string = "python"

if re.search(re.escape(sub_string), full_string, re.IGNORECASE):
    print("Found!")
else:
    print("Not found!")

Output:

Found!

re.search() returns a Match object on success and None otherwise, and Python treats None as falsy, so the result drops straight into an if condition. Wrapping the substring in re.escape() protects the search when the substring itself contains regex metacharacters like a period or a dollar sign, which would otherwise change what the pattern matches.

Performing case-insensitive substring checks

Every method above is case-sensitive by default, which surprises developers coming from languages that treat text more loosely. The most common fix converts both the string and the substring to the same case with lower() or upper() before comparing.

full_string = "Codeforgeek Publishes Python Tutorials"
sub_string = "PYTHON"

if sub_string.lower() in full_string.lower():
    print("Found regardless of case")

Output:

Found regardless of case

lower() and upper() both return a new string rather than modifying the original, since Python strings are immutable. Calling lower() on both sides guards against every casing combination without writing separate checks for each one. re.search() offers the same result through the re.IGNORECASE flag shown in the previous section, which avoids creating two extra string copies when the source text is large.

Counting and locating every occurrence of a substring

A plain existence check answers “is it there,” but real code often needs to know how many times a substring shows up or where each occurrence sits. The count() method returns the total number of non-overlapping matches.

text = "the cat sat on the mat with another cat"
print(text.count("cat"))

Output:

2

To find every position rather than just the first one, pass a starting index into find() or index() and search again from just past the previous match.

text = "the cat sat on the mat with another cat"
start = 0
positions = []

while True:
    position = text.find("cat", start)
    if position == -1:
        break
    positions.append(position)
    start = position + 1

print(positions)

Output:

[4, 37]

This pattern comes up often when scanning log files or user input for repeated keywords, where a single match is not enough context to act on.

Checking for a substring in a pandas DataFrame column

Working with a column of text instead of a single string calls for pandas rather than a Python for loop over every row. The .str.contains() method applies a substring check across an entire column in one call and returns a boolean Series that can filter the DataFrame directly.

import pandas as pd

data = {"title": ["Python for loops", "JavaScript closures", "Python decorators"]}
df = pd.DataFrame(data)

python_rows = df[df["title"].str.contains("Python")]
print(python_rows)

Output:

              title
0   Python for loops
2  Python decorators

.str.contains() accepts the same case and regex options as re.search(), including case=False for a case-insensitive match and na=False to treat missing values as no match instead of raising an error. This is the method to reach for once substring checks move from a single string to a full dataset, the same way appending rows in pandas replaces a manual for loop once the data grows past a handful of items.

Checking if any string in a list contains a substring

A single in check only tells you about one string at a time. Filtering a list of strings for a substring match calls for a list comprehension or the built-in any() function, depending on whether the result needed is the matching items or a plain true/false answer.

titles = ["Python for loops", "JavaScript closures", "Python decorators"]

matches = [title for title in titles if "Python" in title]
print(matches)

Output:

['Python for loops', 'Python decorators']

The list comprehension above collects every matching item. When only a yes or no answer is needed, wrap the same check in any() instead, which stops at the first match rather than scanning the whole list.

titles = ["Python for loops", "JavaScript closures", "Python decorators"]

if any("Python" in title for title in titles):
    print("At least one title mentions Python")

Output:

At least one title mentions Python

any() short-circuits as soon as it finds a True value, so it runs faster than building a full list first and checking its length. Use all() instead of any() when every item in the list needs to contain the substring rather than just one of them.

Comparing the substring check methods

The table below lines up every method by return type, behavior on a missing substring and the situation it fits best.

MethodReturnsOn no matchBest for
inBooleanFalseQuick existence check
find()Integer-1Position lookup without exceptions
index()IntegerRaises ValueErrorCases where a missing substring is a bug
operator.contains()BooleanFalseFunctional-style code, filter(), map()
re.search()Match object or NoneNonePattern matching, case-insensitive search
str.contains() (pandas)Boolean SeriesFalse per rowFiltering a DataFrame column
any() / all()BooleanFalse (any) / True (all)Checking a substring across a list of strings

For most day-to-day code, in stays the right default. Reach for the others only once the plain check stops covering what the code needs.

Key takeaways

  • The in operator is the fastest and most readable way to check for a substring
  • find() returns -1 on a miss, index() raises ValueError instead
  • operator.contains() is a function form of in for filter() and map()
  • re.search() with re.IGNORECASE handles pattern and case-insensitive matches
  • lower() or upper() on both sides removes case sensitivity from any method
  • pandas str.contains() filters a whole DataFrame column in one call
  • any() with a generator expression checks a whole list for at least one match

Frequently asked questions

What is the fastest way to check if a Python string contains a substring?

The in operator is the fastest and most readable choice for a plain existence check. It returns True or False directly and needs no imports or error handling.

How do I make a substring check case-insensitive?

Convert both the string and the substring to the same case with lower() or upper() before comparing, or use re.search() with the re.IGNORECASE flag for pattern-based checks.

How do I find the position of a substring instead of just checking if it exists?

Use find(), which returns the starting index of the first match or -1 if the substring is missing. index() works the same way but raises ValueError instead of returning -1.

Can I check if a string contains any of several substrings at once?

Yes. Combine multiple in checks with the or keyword, or use re.search() with a pattern like “cat|dog” where the pipe character matches either option.

Why does my substring check raise a ValueError?

index() raises ValueError when the substring is not found, unlike find() which returns -1. Wrap index() in a try and except block, or switch to find() instead.

How do I check for a substring across an entire pandas column?

Use df[column].str.contains(substring), which returns a boolean Series marking every row where the substring appears. Pass case=False for a case-insensitive match.

Does the order of the substring and the string matter with in?

Yes. The syntax is substring in string, not the other way around. Writing string in substring silently checks the reverse relationship and returns a different, often wrong, result.

Conclusion

The in operator covers most substring checks in Python, with find(), index(), operator.contains() and re.search() available once the situation calls for a position, an exception, a functional interface or a pattern match. Case sensitivity, counting occurrences and pandas columns extend the same core idea to messier, everyday text. Picking the right method up front saves a rewrite later, once a script that started with a single string ends up filtering a whole column of them.

Aditya Gupta
Aditya Gupta

Aditya Gupta is a founding member and editor at CodeForGeek. He first found his way into tech by reading articles, and now writes approachable guides to Node.js security, authentication, AI tools, coding agents, and web scraping.

Articles: 529