Find Palindromes in a Text File with Python
Scan a text file for palindrome words or complete lines with a tested Python program. Preserve source spelling, report line numbers, and handle encodings safely.
To find palindromes in a text file with Python, start with clear matching rules and results you can trace. This Python approach reads one line at a time, examines either ASCII word tokens or complete lines, and prints the original spelling with its line number. That makes results easy to inspect without rewriting the input.
A palindrome reads the same in both directions after applying a stated set of rules. Level can match case-insensitively; Never odd or even. can match after spaces and punctuation are ignored. A number such as 12321 can match in line mode too. For a manual introduction, see checking a palindrome with a text reverser. This guide focuses on file searching rather than repeating the Python string reversal tutorial.
What this file scanner will do
The default words mode extracts ASCII letter runs with re.findall(r'[A-Za-z]+', line). Hyphens and apostrophes separate tokens: step-on becomes step and on, while can't becomes can and t. Non-ASCII characters also act as boundaries. This is a simple policy for ASCII word data, not a universal word tokenizer.
An accented word is not examined as a complete word in this mode: café contributes caf, and other inputs can produce matching ASCII fragments. Use it only when those boundaries suit your data, or replace the tokenizer. Whole-line mode is available when you want a different comparison unit.
The lines mode treats each complete line as one candidate. It applies NFC normalization, case folding, and then retains characters for which isalnum() is true. Spaces and punctuation do not affect that comparison. Results still show the original line with its case and punctuation.
Both modes require a normalized length of at least two by default, excluding trivial one-letter matches. --min-length 3 also excludes aa. Repeated occurrences remain separate results so their source locations are visible. The program does not search every substring or find the longest palindromic substring.
Save the complete Python program
Save this as find_palindromes.py. It uses only the standard library and works with Python 3.9 or newer. The Path.open documentation describes the path-based file-opening method used below. The file is opened in read-only text mode:
#!/usr/bin/env python3
import argparse
import pathlib
import re
import sys
import unicodedata
def clean_line(text):
normalized = unicodedata.normalize("NFC", text).casefold()
return "".join(character for character in normalized if character.isalnum())
def is_palindrome(candidate, minimum_length):
return len(candidate) >= minimum_length and candidate == candidate[::-1]
def positive_integer(value):
try:
number = int(value)
except ValueError as error:
raise argparse.ArgumentTypeError("must be an integer") from error
if number < 1:
raise argparse.ArgumentTypeError("must be a positive integer")
return number
def build_parser():
parser = argparse.ArgumentParser(
description="Find palindrome words or lines in a text file."
)
parser.add_argument("path", type=pathlib.Path, help="input text file (read only)")
parser.add_argument(
"--mode", choices=("words", "lines"), default="words",
help="scan ASCII word tokens or complete normalized lines (default: words)",
)
parser.add_argument(
"--min-length", type=positive_integer, default=2,
help="minimum normalized candidate length (default: 2)",
)
parser.add_argument(
"--encoding", default="utf-8-sig",
help="text encoding used to read the file (default: utf-8-sig)",
)
return parser
def scan_file(path, mode, minimum_length, encoding):
with path.open("r", encoding=encoding, newline=None) as handle:
for line_number, line in enumerate(handle, 1):
original_line = line.rstrip("\r\n")
if mode == "words":
for word in re.findall(r"[A-Za-z]+", original_line):
if is_palindrome(word.casefold(), minimum_length):
yield line_number, word
else:
normalized_line = clean_line(original_line)
if is_palindrome(normalized_line, minimum_length):
yield line_number, original_line
def main(argv=None):
parser = build_parser()
args = parser.parse_args(argv)
try:
for line_number, original in scan_file(
args.path, args.mode, args.min_length, args.encoding
):
print(f"{line_number}: {original}")
except (OSError, UnicodeError, LookupError) as error:
print(f"{parser.prog}: {error}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
The main() guard prevents importing the module from unexpectedly scanning a path. The argparse library supplies help, a positional path, and rejection of unsupported modes. The custom integer conversion rejects non-integer, zero, or negative minimum lengths with a command-line error rather than silently accepting a nonsensical threshold.
Create a sample text file
Create sample.txt with exactly these five lines:
Level, radar and civic.
Python is useful.
Never odd or even.
Level again.
12321
Run the default word scan from the folder containing both files:
python3 find_palindromes.py sample.txt
On Windows, py -3 find_palindromes.py sample.txt is a common equivalent when the Python launcher is installed. If your installation uses python instead of python3, confirm that it starts Python 3 before using that name.
Expected output:
1: Level
1: radar
1: civic
4: Level
Level appears twice because the source contains two occurrences. Output retains the spelling found in the file instead of replacing it with a lowercase key. The phrase on line three is not a matching word token: its individual words are not palindromes. The numeric line is not an ASCII letter token either.
The line number refers to the source file, not the position in the results. Several words can therefore have the same line number. This makes repeated matches traceable even though the simple report does not include character offsets or distinguish identical tokens repeated within one line.
Scan complete lines instead
Use line mode when your definition is “the whole line, ignoring case, whitespace, and punctuation”:
python3 find_palindromes.py sample.txt --mode lines
Expected output:
3: Never odd or even.
5: 12321
Only line endings are removed before printing; the period remains in the displayed phrase. Internally, it becomes neveroddoreven. A negative-looking value such as -121 becomes 121, because the minus sign is excluded by the cleaning policy. This is text comparison, not an integer-sign validator.
The test candidate == candidate[::-1] compares the candidate with a reversed copy. Python’s sequence operations documentation explains slicing with a negative step. For a candidate of length m, reversal and comparison take linear work in the worst case, and the slice requires additional storage. The script is not a constant-memory algorithm for arbitrarily long candidates.
If you change the cleaning policy, change the expected results too. For example, requiring punctuation to match would reject the displayed phrase with its final period, while a numeric-only policy could reject alphabetic lines altogether. Matching rules should be part of the program’s contract, not a hidden assumption in the comparison.
Understand the normalization rules
NFC canonical normalization composes equivalent sequences where appropriate. It is not accent stripping. The unicodedata.normalize documentation explains the distinction between composed and decomposed representations. Applying NFC before filtering helps canonically equivalent accented text reach the same representation.
Next, casefold() provides caseless matching stronger than lower(). Python’s casefold documentation gives the example of straße becoming strasse. Length can therefore change. The script applies the minimum after cleaning, so it measures normalized comparison characters, not the original number of printed characters.
Finally, isalnum() retains Unicode letters and numbers. Remaining combining marks and emoji components are not generally preserved by that filter. NFC itself does not strip accents, but the later filtering step can discard marks that remain separate. This is a transparent code-point policy, not a guarantee to preserve every accent distinction or treat every visible grapheme as one unit.
For multilingual or grapheme-sensitive applications, choose a tokenizer and comparison unit suited to the language. Merely changing the ASCII regular expression to a broader-looking pattern does not solve word boundaries, normalization, and grapheme segmentation together. Keep the small scanner’s limitations visible when reusing it.
Read the right encoding and line endings
The default utf-8-sig reads ordinary UTF-8 and consumes an optional UTF-8 byte-order mark at the beginning. A .txt extension does not prove which encoding was used. When the creating application used a different known encoding, select it explicitly:
python3 find_palindromes.py notes.txt --encoding cp1252
Do not use errors="ignore" merely to silence a decoding error. Silently discarded bytes can change candidates and produce misleading palindrome decisions. An unknown encoding name is also caught and reported as a short error rather than a traceback.
Text mode with newline=None recognizes common newline conventions, including LF and CRLF. The explicit rstrip("\r\n") removes those ending characters without stripping internal spaces or punctuation. The with block closes the file when scanning finishes or an error occurs.
An input error can happen after earlier matches have already printed. In that situation the output may be partial, and the command exits unsuccessfully. Check that a scan completed without an error before treating a redirected report as a complete result. Correct the encoding or input problem and rerun rather than merging uncertain partial output.
Process large files without risking the source
The script iterates over the open file instead of reading the entire document into one string. It yields and prints matches rather than retaining a growing results list. Memory still depends on the current line, its extracted tokens, and normalized or reversed copies. Millions of short lines and one enormous line are different workloads.
Python’s re.findall documentation describes the non-overlapping, left-to-right matches used by word mode. It returns the matching tokens for the current line, so this is not a fully streaming character-by-character tokenizer. That is a reasonable trade-off for normal text files, provided the line-size limitation is understood.
The script opens input only for reading, but the shell can still destroy it if you redirect output to the same path. Never run python3 find_palindromes.py sample.txt > sample.txt: redirection can truncate the source before Python opens it. Choose a different destination:
python3 find_palindromes.py sample.txt > palindromes.txt
Quote a path containing spaces, such as python3 find_palindromes.py "Text exports/january notes.txt". Also remember that ordinary > redirection can overwrite an existing report. Choose a new report name when previous results need to be retained.
Duplicates are deliberate. If you later need unique normalized matches, maintain a set of comparison keys, but account for the extra memory and decide which original spelling or source locations to preserve. Deduplication changes the report’s meaning; it should not be added invisibly.
Check the rules before scanning your own data
The complete scanner was tested against the sample output, BOM and CRLF files, duplicates, minimum thresholds, normalization, input errors, and unchanged source bytes. You can also save this small regression check as check_palindromes.py beside the scanner:
from find_palindromes import clean_line, is_palindrome
assert is_palindrome("level", 2)
assert is_palindrome("aa", 2)
assert not is_palindrome("aa", 3)
assert not is_palindrome("a", 2)
assert not is_palindrome("", 2)
assert not is_palindrome("python", 2)
assert is_palindrome(clean_line("Never odd or even."), 2)
assert clean_line("-121") == "121"
print("Basic palindrome checks passed.")
Run python3 check_palindromes.py without optimization flags. A successful run prints Basic palindrome checks passed. The explicit empty-candidate and minimum-length checks prevent blank or trivial inputs from becoming surprising matches. These checks complement, rather than replace, the file-based sample runs.
If a phrase is missing, check the mode first. If an accented word is split, check the ASCII tokenization policy. If nothing prints, distinguish a successful scan with no qualifying candidates from an error message on standard error. Missing paths, permissions, decoding failures, and unknown encodings produce a nonzero exit status.
The scanner finds complete tokens or complete normalized lines, not every possible substring. With those boundaries understood, it provides a repeatable way to inspect palindrome candidates without modifying a document. For one-off browser-based transformations instead of a file workflow, visit the Text Reverser tool.
Try the Text Reverser tool
Reverse text, words, or letters instantly — free and entirely in your browser.
Open the tool