Python startswith() Method: Prefix Checks, Tuples and Ranges

Use Python str.startswith() to check prefixes, filter log lines with a tuple, skip fields with start and end, and compare removeprefix() and slicing.

Python

The Python str.startswith() method returns True when a string begins with the given prefix and False otherwise. Reading log files is a good example, where we keep the lines that start with ERROR and skip the lines that start with #.

line = "ERROR disk is full"
is_error = line.startswith("ERROR")     # True
is_info = line.startswith("INFO")       # False

We filter a small log with a tuple of prefixes, skip a date field with start, ignore case, match prefixes with regular expressions, and compare startswith() with removeprefix() and slicing. We also check both ends of a string at once. The code was checked on Python 3.14.

1. Syntax and Return Value

Like its sibling endswith(), the method takes a prefix and two optional indexes, and it is case-sensitive.

str.startswith(prefix[, start[, end]]) -> bool
  • prefix is a string or a tuple of strings.
  • start is the index where the check begins (default 0).
  • end is the index where the check stops; the character at end is not included.

The return value is a bool. The method returns either True, when the string (or the slice str[start:end]) begins with the prefix or with any prefix in the tuple, or False otherwise.

2. Filtering Log Lines with a Tuple of Prefixes

A tuple lets one call match several prefixes. In a log file, that picks out every warning and error line in a single list comprehension.

log = ["INFO started", "WARN low memory", "ERROR disk is full", "INFO stopped"]
problems = [line for line in log if line.startswith(("WARN", "ERROR"))]
# ['WARN low memory', 'ERROR disk is full']

As with endswith(), a list of prefixes must be wrapped with tuple(prefixes), or the call raises TypeError.

3. Checking from a Position

When a line has a fixed-width field in front, such as a date, start moves the check past it. Because end is exclusive, a range that is too short cuts the prefix and the result becomes False.

line = "2026-10-09 ERROR disk is full"
at_11 = line.startswith("ERROR", 11)        # True
short = line.startswith("ERROR", 11, 14)    # False, the slice is "ERR"

4. Case-Insensitive Checks and Comment Lines

User input and file content rarely have a fixed case or fixed indentation. We normalize the string first with lower() or lstrip(), and call startswith() on the result.

asks_help = "Help me".lower().startswith("help")   # True

config = ["# timeout in seconds", "timeout=30", "", "  # retries", "retries=3"]
settings = 
# ['timeout=30', 'retries=3']

5. startswith() vs removeprefix() vs Slicing

These approaches all look at the beginning of a string, but they return different things. We use startswith() for a yes-or-no answer and removeprefix() (Python 3.9+) when we need the rest of the string.

url = "https://example.com"
secure = url.startswith("https://")       # True
host = url.removeprefix("https://")       # 'example.com'
same = url[:8] == "https://"              # True, but the 8 must match the prefix length
ApproachReturnsWatch out for
s.startswith(p)True or FalseCase-sensitive
s.removeprefix(p)The string without the prefixReturns the string unchanged when the prefix is missing
s[:n] == pTrue or FalseA wrong n gives a silent bug
re.match(pattern, s)A match object or NoneOnly worth it for patterns, not fixed text

6. Matching a Prefix with Regular Expressions

The re.match() function matches only at the start of a string, so it needs no anchor. The re.search() function looks anywhere in the string, so it needs the caret ^ to anchor at the start. Regular expressions are for prefixes that are patterns, such as two letters followed by a dash, and re.IGNORECASE makes the match case-insensitive.

import re

text = "HowToDoInJava.com"
by_match = re.match("How", text) is not None                  # True
by_search = re.search("^How", text) is not None              # True
by_pattern = re.match(r"[A-Z]{2}-", "AB-1042") is not None   # True
ignore_case = re.match("how", text, re.IGNORECASE) is not None   # True

We wrap fixed text in re.escape() before putting it into a pattern, because characters such as . have a special meaning. Without it, the pattern .com matches the start of xcom.net, since the dot stands for any character.

loose = re.match(".com", "xcom.net") is not None                # True, wrong
exact = re.match(re.escape(".com"), "xcom.net") is not None     # False

7. Checking the End of a String and Both Ends

The endswith() method from section 1 checks the end of a string with a suffix and the same optional start and end indexes. Some formats need both checks, such as a quoted value or a bracketed tag. We combine the two methods with and, and re.fullmatch() checks the whole string in one pattern.

value = '"Lokesh"'
quoted = value.startswith('"') and value.endswith('"')       # True
inner = value[1:-1] if quoted else value                     # 'Lokesh'
unwrapped = value.removeprefix('"').removesuffix('"')        # 'Lokesh'
tag = re.fullmatch(r"\[.*\]", "[ERROR]") is not None         # True

url = "https://example.com/report.pdf"
secure_pdf = url.startswith("https://") and url.endswith(".pdf")   # True

A regular expression anchors at the end of a string with \Z. The $ anchor also matches before a final newline, so it can report a match that endswith() would reject.

ends_dollar = re.search(r"com$", "site.com\n") is not None   # True, $ matches before the final newline
ends_z = re.search(r"com\Z", "site.com\n") is not None       # False, \Z means the real end

8. Routing Chat Commands by Prefix

A chat bot treats messages that start with a slash as commands. The startswith() check routes the message, and partition() splits the command name from its arguments in one call.

def parse(message):
    if not message.startswith("/"):
        return None, message
    command, _, args = message[1:].partition(" ")
    return command.lower(), args

r1 = parse("/remind 10m tea")     # ('remind', '10m tea')
r2 = parse("/HELP")               # ('help', '')
r3 = parse("hello there")         # (None, 'hello there')

The same pattern works for any line-based protocol, such as reading commands from a file or a socket, where a prefix decides how the rest of the line is handled.

9. Prefix Check Questions

A plain startswith() call stops being enough once the prefix is a pattern or the data is bytes.

9.1. Does startswith() work with bytes?

Yes. A bytes object has its own startswith(), so a file signature check reads data.startswith(b”%PDF”). The prefix must also be bytes.

9.2. How do we check whether a string starts with a number?

We test the first character with isdigit(). The slice s[:1] returns an empty string instead of raising IndexError when s is empty.

starts_num = "3 apples"[:1].isdigit()    # True
empty_ok = ""[:1].isdigit()               # False

9.3. Does an empty string count as a prefix?

Yes. The call text.startswith(“”) returns True for every string, which matters when the prefix comes from user input that can be empty.

9.4. Which prefix check is the fastest?

For fixed text, startswith() is the fastest and slicing is close. A regular expression is slower unless the pattern is compiled once with re.compile() and reused. In most code the clearer call decides, and that is startswith().

10. Conclusion

For log filters and config readers, startswith() with a tuple of prefixes is shorter and safer than slicing. We normalize the line with lower() or lstrip() before the check, and we switch to removeprefix() when we need the rest of the text. For prefixes that are patterns we use re.match() with re.escape() around fixed text, and endswith() covers the end of the string.

11. References

Happy Learning !!

Source Code on Github

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.