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
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.
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.
- 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)].
- Forgetting case.
"IMG_2026.JPG".removesuffix(".jpg") changes nothing. Compare with name.lower().endswith(".jpg") first.
- Expecting
splitext to strip .tar.gz whole. It will not — you get ('archive.tar', '.gz'). Spell the double suffix out yourself.
- 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("").
- 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.
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, useos.path.splitext(name)[0]orPath(name).stem. Never userstrip(".txt")for this: it silently eats extra letters and turnsreport.txtintorepor.print("report.txt".removesuffix(".txt")) print("report.txt".rstrip(".txt"))Below: why that happens, how the four correct approaches differ, and what to do about
archive.tar.gz,.gitignoreand files with no extension at all.Why does
"report.txt".rstrip(".txt")returnrepor?Because
rstripdoes not take an ending, it takes a set of characters. It splits the argument".txt"into three distinct characters — a dot,tandx— and keeps chewing characters off the right end while they belong to that set. The namereportends int, 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")andrstrip("xt.")behave identically.name = "report.txt" print(name.rstrip(".txt"))Step by step
.,t,x?report.txttreport.txxreport.ttreport..reporttreporrThe nasty part is that
rstripsometimes 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])rstrip(".txt")returnedreport.txtreporreporttest.txttestestphoto.txtphotophotonotes.txtnotesnotesNon-Latin names hide the problem even better:
"отчёт.txt".rstrip(".txt")returnsотчёт, because Cyrillicтis a different character from Latint. 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"))Look at the third line:
report.csvis 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)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 theendswith()plus slice combination above: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 partsWhen 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)One important detail:
splitextlooks 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"))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])pathlib:Path("report.txt").stemandwith_suffix()If your code already deals with paths,
pathlibis the shortest option:.stemis the file name without the extension,.suffixis the extension,.nameis the full file name, andwith_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"))Notice that
stemreturned the name only, with no directories. If the result is going straight intoopen(), you wantp.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 likename[:-4]for the rare case where the extension length is guaranteed.name.removesuffix(".txt")os.path.splitext(name)('report', '.txt')Path(name).stem'report'pathlibname.rsplit(".", 1)[0]name[:-4]name.rstrip(".txt")The slice deserves a warning of its own, because it fails the quietest:
print("report.txt"[:-4]) print("photo.jpeg"[:-4]).jpegis 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 withsplit(".")[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])What does
splitext()return forarchive.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.tarstays part of the name. For the same reasonPath("archive.tar.gz").stemgivesarchive.tar. If you want both pieces gone in one move, name the suffix explicitly withremovesuffix(".tar.gz"), or inspectPath(...).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"))Chaining works too, when you want to peel the tails one at a time:
print("report.tar.gz".removesuffix(".gz").removesuffix(".tar"))Files with no extension and names that start with a dot
.gitignore,.envandREADMEare whole names with no extension. Bothsplitextandpathlibtreat 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"))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")andremovesuffix("\n")work, but they diverge when there are two:rstripremoves both,removesuffixremoves exactly one.rstripis harmless here because its set holds a single character that never occurs inside the data. The danger starts precisely when the argument to astrip-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")))While we are here, a neighbouring myth: a trailing
\ndoes not break number parsing on its own.float()discards whitespace at both ends of the string, sofloat("77.59\n")returns77.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
rstrip(".txt")instead ofremovesuffix(".txt"). The classic: letters vanish from the file name and your logs holdreporinstead ofreport. Fix it withremovesuffix(".txt"), oros.path.splitext(name)[0]on Python older than 3.9.split(".")[0]instead ofrsplit(".", 1)[0]. Onprices.2026.csvthe first one returnspricesand loses the year; on.gitignoreit returns an empty string.name[:-4]. Fine for.txtand.csv, broken for.jpeg,.xlsxand extensionless files. If you must slice, compute the length:name[:-len(ext)]."IMG_2026.JPG".removesuffix(".jpg")changes nothing. Compare withname.lower().endswith(".jpg")first.splitextto strip.tar.gzwhole. It will not — you get('archive.tar', '.gz'). Spell the double suffix out yourself.stemandwith_suffix("").Path("/home/anna/docs/report.txt").stemisreportwith the directories gone; if the value is used as a path afterwards, you needwith_suffix("").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])Read the output carefully:
archive.tarand.gitignoreare 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,xrather than an ending, soreport.txtbecomesrepor.rstripguesses right (photo.txt,notes.txt), which is why the bug survives so long.name.removesuffix(".txt"), Python 3.9 and newer.os.path.splitext(name); already holding a path —Path(name).stem.name.rsplit(".", 1)[0];split(".")[0]is the wrong tool.archive.tar.gzyields('archive.tar', '.gz')and.gitignorehas no extension — both are correct.Path(name).with_suffix(".csv"), not string concatenation.