Pythonstringspathlibfiles

How to Remove a File Extension From a String in Python

rstrip('.txt') does not remove an extension: it chews a character set, so report.txt silently becomes repor. Here is the mechanism plus four correct approaches — removesuffix, os.path.splitext, Path.stem and slicing. Edge cases too: archive.tar.gz, .gitignore and extensionless files.

8 min readReferencePython · strings · pathlib · files · beginners

To remove a file extension from a string in Python, use name.removesuffix(".txt") — it cuts exactly that ending and nothing else. If you do not know the extension in advance, use os.path.splitext(name)[0] or Path(name).stem. Never use rstrip(".txt") for this: it silently eats extra letters and turns report.txt into repor.

print("report.txt".removesuffix(".txt"))
print("report.txt".rstrip(".txt"))
report
repor

Below: why that happens, how the four correct approaches differ, and what to do about archive.tar.gz, .gitignore and files with no extension at all.

Why does "report.txt".rstrip(".txt") return repor?

Because rstrip does not take an ending, it takes a set of characters. It splits the argument ".txt" into three distinct characters — a dot, t and x — and keeps chewing characters off the right end while they belong to that set. The name report ends in t, that letter is in the set too, so it disappears right after the extension. The order of characters in the argument means nothing: rstrip(".txt") and rstrip("xt.") behave identically.

name = "report.txt"
print(name.rstrip(".txt"))
repor

Step by step

Step String Last character In the set ., t, x?
1 report.txt t yes, cut it
2 report.tx x yes, cut it
3 report.t t yes, cut it
4 report. . yes, cut it
5 report t yes, cut it
6 repor r no — stop

The nasty part is that rstrip sometimes returns the right answer, so the bug can sit in your code for months until an "unlucky" name shows up:

files = ["report.txt", "test.txt", "photo.txt", "notes.txt"]
print([f.rstrip(".txt") for f in files])
['repor', 'tes', 'photo', 'notes']
File name What rstrip(".txt") returned What you wanted Match?
report.txt repor report no
test.txt tes test no
photo.txt photo photo yes, by accident
notes.txt notes notes yes, by accident

Non-Latin names hide the problem even better: "отчёт.txt".rstrip(".txt") returns отчёт, because Cyrillic т is a different character from Latin t. The code looks fine on those names and breaks on the first English one.

How do you remove a file extension from a string in Python with removesuffix()?

Use str.removesuffix(".txt"), available since Python 3.9. It does exactly what you expect: if the string ends with the given suffix, the whole suffix is cut off; if it does not, the string comes back untouched. That second half is what makes it safe in a loop over mixed file names.

print("report.txt".removesuffix(".txt"))
print("test.txt".removesuffix(".txt"))
print("report.csv".removesuffix(".txt"))
report
test
report.csv

Look at the third line: report.csv is unchanged because the suffix did not match. There is a mirror method, removeprefix(), for the other end of the string.

One catch: the method is case sensitive.

name = "IMG_2026.JPG"
print(name.removesuffix(".jpg"))

if name.lower().endswith(".jpg"):
    name = name[:-len(".jpg")]
print(name)
IMG_2026.JPG
IMG_2026

On Python 3.8 and older the method does not exist at all, and you get the line below instead of a result — fall back to os.path.splitext() or to the endswith() plus slice combination above:

AttributeError: 'str' object has no attribute 'removesuffix'

Both methods are covered in the handbook card Remove exact prefix or suffix. A neighbouring move — cutting a tail off by a separator — is drilled in the free task Link without the anchor.

os.path.splitext(): name and extension as separate parts

When you need both halves, reach for os.path.splitext(). It splits at the last dot and always returns a tuple of two strings.

import os

print(os.path.splitext("report.txt"))

base, ext = os.path.splitext("prices_2026.csv")
print(base, ext)
('report', '.txt')
prices_2026 .csv

One important detail: splitext looks at the file name, not at the whole path, so dots inside directory names do not confuse it.

import os

print(os.path.splitext("data/2026.report/notes"))
('data/2026.report/notes', '')

If you only want the bare name out of a full path, add os.path.basename():

import os

path = "/home/anna/docs/report.txt"
print(os.path.basename(path))
print(os.path.splitext(os.path.basename(path))[0])
report.txt
report

pathlib: Path("report.txt").stem and with_suffix()

If your code already deals with paths, pathlib is the shortest option: .stem is the file name without the extension, .suffix is the extension, .name is the full file name, and with_suffix() replaces the extension without any manual string concatenation.

from pathlib import Path

p = Path("/home/anna/docs/report.txt")
print(p.name)
print(p.stem)
print(p.suffix)
print(p.with_suffix(".csv"))
report.txt
report
.txt
/home/anna/docs/report.csv

Notice that stem returned the name only, with no directories. If the result is going straight into open(), you want p.with_suffix("") instead — it keeps the whole path. More on path objects in the handbook: Path objects.

Which should you use: a slice, removesuffix, splitext or pathlib?

The rule of thumb is short. You know the extension up front — removesuffix. You need both halves — splitext. You already hold a path object — Path.stem. The extension is unknown and you would rather not import anything — rsplit(".", 1)[0]. Keep a hard slice like name[:-4] for the rare case where the extension length is guaranteed.

Approach What it does When to use it
name.removesuffix(".txt") cuts the exact suffix or nothing suffix known, Python 3.9+
os.path.splitext(name) returns ('report', '.txt') you need both halves
Path(name).stem returns 'report' you already use pathlib
name.rsplit(".", 1)[0] splits at the last dot extension unknown in advance
name[:-4] cuts exactly 4 characters extension length is fixed
name.rstrip(".txt") chews a character set never for this task

The slice deserves a warning of its own, because it fails the quietest:

print("report.txt"[:-4])
print("photo.jpeg"[:-4])
report
photo.

.jpeg is five characters long and the slice cut four, so a dot stayed in the name. Slicing rules are worth a full read: String slicing.

And rsplit(".", 1)[0] is the safe one-liner: it splits at the last dot and does not blow up when there is no dot at all. Do not confuse it with split(".")[0], which splits at the first one:

print("prices.2026.csv".rsplit(".", 1)[0])
print("README".rsplit(".", 1)[0])
print("prices.2026.csv".split(".")[0])
prices.2026
README
prices

What does splitext() return for archive.tar.gz?

It returns ('archive.tar', '.gz'), and that is not a bug. A double extension is a human convention, not a filesystem rule: for Python the extension starts after the last dot, so .tar stays part of the name. For the same reason Path("archive.tar.gz").stem gives archive.tar. If you want both pieces gone in one move, name the suffix explicitly with removesuffix(".tar.gz"), or inspect Path(...).suffixes.

import os
from pathlib import Path

print(os.path.splitext("archive.tar.gz"))
print(Path("archive.tar.gz").stem)
print(Path("archive.tar.gz").suffixes)
print("archive.tar.gz".removesuffix(".tar.gz"))
('archive.tar', '.gz')
archive.tar
['.tar', '.gz']
archive

Chaining works too, when you want to peel the tails one at a time:

print("report.tar.gz".removesuffix(".gz").removesuffix(".tar"))
report

Files with no extension and names that start with a dot

.gitignore, .env and README are whole names with no extension. Both splitext and pathlib treat a leading dot as part of the name rather than as a separator, and both report an empty extension.

import os
from pathlib import Path

print(os.path.splitext(".gitignore"))
print(repr(Path(".gitignore").stem))
print(repr(Path(".gitignore").suffix))
print(os.path.splitext("README"))
('.gitignore', '')
'.gitignore'
''
('README', '')

A hand-rolled ".gitignore".split(".")[0], on the other hand, returns an empty string — real data loss that is easy to miss inside a loop over a hundred files.

How do you strip a trailing newline without touching the data?

For a single guaranteed newline both rstrip("\n") and removesuffix("\n") work, but they diverge when there are two: rstrip removes both, removesuffix removes exactly one. rstrip is harmless here because its set holds a single character that never occurs inside the data. The danger starts precisely when the argument to a strip-family method holds more than one character, as ".txt" does.

line = "report.txt\n"
print(repr(line.rstrip("\n")))
print(repr(line.removesuffix("\n")))
print(repr("100\n\n".rstrip("\n")))
print(repr("100\n\n".removesuffix("\n")))
'report.txt'
'report.txt'
'100'
'100\n'

While we are here, a neighbouring myth: a trailing \n does not break number parsing on its own. float() discards whitespace at both ends of the string, so float("77.59\n") returns 77.59. What stops "77,59\n" from converting is the comma, not the newline. The real causes of that error are covered separately in could not convert string to float. Careful whitespace cleanup is drilled in the free task Tidy spaces.

Common mistakes

  1. rstrip(".txt") instead of removesuffix(".txt"). The classic: letters vanish from the file name and your logs hold repor instead of report. Fix it with removesuffix(".txt"), or os.path.splitext(name)[0] on Python older than 3.9.
  2. split(".")[0] instead of rsplit(".", 1)[0]. On prices.2026.csv the first one returns prices and loses the year; on .gitignore it returns an empty string.
  3. A hard slice, name[:-4]. Fine for .txt and .csv, broken for .jpeg, .xlsx and extensionless files. If you must slice, compute the length: name[:-len(ext)].
  4. Forgetting case. "IMG_2026.JPG".removesuffix(".jpg") changes nothing. Compare with name.lower().endswith(".jpg") first.
  5. Expecting splitext to strip .tar.gz whole. It will not — you get ('archive.tar', '.gz'). Spell the double suffix out yourself.
  6. Mixing up stem and with_suffix(""). Path("/home/anna/docs/report.txt").stem is report with the directories gone; if the value is used as a path afterwards, you need with_suffix("").
  7. Reading a list of names from a file without encoding="utf-8". The names arrive as mojibake and every slice then operates on the wrong characters. See mojibake and UnicodeDecodeError.

Practice: clean up a list of exported file names

Put it together — a list of exported names is cleaned in one line:

from pathlib import Path

files = ["report.txt", "prices_2026.csv", "archive.tar.gz", ".gitignore", "README"]
print([Path(f).stem for f in files])
['report', 'prices_2026', 'archive.tar', '.gitignore', 'README']

Read the output carefully: archive.tar and .gitignore are not mistakes, they are exactly the behaviour described above. To drill string methods by hand with automatic checking, start with the free string tasks — the code runs right in the browser.

In short

  • rstrip(".txt") strips the character set ., t, x rather than an ending, so report.txt becomes repor.
  • Sometimes rstrip guesses right (photo.txt, notes.txt), which is why the bug survives so long.
  • Extension known in advance — name.removesuffix(".txt"), Python 3.9 and newer.
  • Need both halves — os.path.splitext(name); already holding a path — Path(name).stem.
  • Extension unknown — name.rsplit(".", 1)[0]; split(".")[0] is the wrong tool.
  • archive.tar.gz yields ('archive.tar', '.gz') and .gitignore has no extension — both are correct.
  • To replace an extension use Path(name).with_suffix(".csv"), not string concatenation.

Practice on real tasks

Solve tasks in the Python trainer with instant grading and hints.

Open trainer