Pythonstringspathlibfiles

Убрать расширение файла из строки в Python: 4 способа

rstrip('.txt') не убирает расширение: он грызёт набор символов, и report.txt тихо превращается в repor. Разбираем механику этого бага и четыре правильных способа — removesuffix, os.path.splitext, Path.stem и срез. Отдельно: archive.tar.gz, .gitignore и файлы вообще без расширения.

7 мин чтенияСправочникPython · strings · pathlib · files · beginners

В Python убрать расширение файла из строки надёжнее всего методом removesuffix(".txt"): он снимает ровно это окончание и ничего больше. Если расширение заранее неизвестно — бери os.path.splitext(name)[0] или Path(name).stem, а вот rstrip(".txt") для этой задачи не годится никогда: он молча съедает лишние буквы, и report.txt превращается в repor.

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

Ниже — почему так происходит, чем отличаются четыре правильных способа и что делать с archive.tar.gz, .gitignore и файлами вообще без расширения.

Почему "report.txt".rstrip(".txt") возвращает repor?

Потому что rstrip принимает не окончание строки, а набор символов. Аргумент ".txt" он разбирает на три разных символа — точку, t и x — и отгрызает с конца всё, что попадает в этот набор, пока не встретит символ вне набора. Имя report заканчивается на t, эта буква тоже в наборе, поэтому она уходит следом за расширением. Порядок символов в аргументе не важен вообще: rstrip(".txt") и rstrip("xt.") работают одинаково.

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

Почему rstrip удаляет лишние символы: разбор по шагам

Шаг Строка Последний символ Есть в наборе ., t, x?
1 report.txt t да, отрезаем
2 report.tx x да, отрезаем
3 report.t t да, отрезаем
4 report. . да, отрезаем
5 report t да, отрезаем
6 repor r нет — стоп

Самое неприятное здесь то, что иногда rstrip выдаёт правильный ответ — и баг живёт в коде месяцами, пока не попадётся «неудачное» имя:

files = ["report.txt", "test.txt", "photo.txt", "notes.txt"]
print([f.rstrip(".txt") for f in files])
['repor', 'tes', 'photo', 'notes']
Имя файла Что вернул rstrip(".txt") Что ожидали Совпало?
report.txt repor report нет
test.txt tes test нет
photo.txt photo photo да, случайно
notes.txt notes notes да, случайно

Кириллица маскирует проблему ещё лучше: "отчёт.txt".rstrip(".txt") вернёт отчёт, потому что русская т — это другой символ, не латинская t. На русских именах код выглядит рабочим, а на первом же английском ломается.

Как в Python убрать расширение файла из строки методом removesuffix()

str.removesuffix() появился в Python 3.9 и делает ровно то, что от него ждут: если строка заканчивается указанным окончанием, метод отрезает его целиком; если нет — возвращает строку нетронутой. Пример на Python 3.12:

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

Посмотри на третью строку: report.csv остался как есть, потому что окончание не совпало. Это и есть правильное поведение для задачи «удалить подстроку с конца строки» — метод либо снимает точный суффикс, либо честно ничего не делает. У него есть зеркальный брат removeprefix().

Один нюанс: метод регистрозависимый.

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

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

На Python 3.8 и старше метода просто нет, и вместо результата будет вот такая строка — тогда бери os.path.splitext() или связку endswith() со срезом, как выше:

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

Оба метода разобраны в карточке справочника «Убрать точный префикс или суффикс». Соседний приём — отрезать хвост по разделителю — тренируется в бесплатной задаче «Ссылка без якоря».

os.path.splitext(): имя и расширение по отдельности

Когда нужны обе части — и имя, и расширение — берётся os.path.splitext(). Он режет по последней точке и всегда возвращает кортеж из двух строк. Вот пример:

import os

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

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

Важная деталь: splitext смотрит на имя файла, а не на весь путь, поэтому точки в названиях папок его не сбивают.

import os

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

Если из полного пути нужно только имя без расширения, добавь 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 и with_suffix()

Если в коде уже есть пути, короче всего pathlib: .stem — это имя файла без расширения, .suffix — расширение, .name — имя целиком, а with_suffix() меняет расширение на другое, не клея строки руками.

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

Обрати внимание: stem вернул только имя, без папок. Если результат дальше пойдёт в open(), тебе нужен не stem, а p.with_suffix("") — он сохраняет весь путь. Подробнее про объекты путей — в справочнике: Path objects.

Что выбрать: срез, removesuffix, splitext или pathlib?

Критерий простой. Знаешь расширение заранее — removesuffix. Нужны обе части — splitext. Уже работаешь с путями — Path.stem. Расширение неизвестно, а тянуть модули не хочется — rsplit(".", 1)[0]. Жёсткий срез вроде name[:-4] оставь для случаев, когда длина расширения гарантирована и не изменится.

Способ Что делает Когда брать
name.removesuffix(".txt") снимает точное окончание или ничего суффикс известен, Python 3.9+
os.path.splitext(name) отдаёт ('report', '.txt') нужны обе части сразу
Path(name).stem отдаёт 'report' в коде уже есть pathlib
name.rsplit(".", 1)[0] режет по последней точке расширение заранее неизвестно
name[:-4] режет ровно 4 символа длина расширения фиксирована
name.rstrip(".txt") грызёт набор символов никогда для этой задачи

Про срез стоит сказать отдельно — он ломается тише всех:

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

У .jpeg пять символов, а срез отрезал четыре — в имени осталась точка. Срезы вообще полезно разобрать целиком: «Срезы строк».

А rsplit(".", 1)[0] — безопасный однострочник: он режет по последней точке и не падает, если точки нет вовсе. Не путай его с split(".")[0], который режет по первой:

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

Что вернёт splitext() для файла archive.tar.gz?

Вернёт ('archive.tar', '.gz'), и это не баг. Двойное расширение — это соглашение между людьми, а не правило файловой системы: для Python расширение начинается после последней точки, поэтому .tar остаётся частью имени. По той же причине Path("archive.tar.gz").stem даёт archive.tar. Если нужно снять оба куска сразу, назови их явно через removesuffix(".tar.gz") или посмотри на список 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

Работает и цепочка, если хвосты нужно снимать по одному:

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

Файл без расширения и имя, которое начинается с точки

.gitignore, .env, README — это имена целиком, расширения у них нет. И splitext, и pathlib считают ведущую точку частью имени, а не разделителем, и оба возвращают пустое расширение.

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', '')

А вот самописный ".gitignore".split(".")[0] вернёт пустую строку — и это уже настоящая потеря данных, которую легко не заметить в цикле по сотне файлов.

Как убрать перенос строки \n в конце, не задев данные?

Для одного гарантированного переноса в конце годятся оба варианта — и rstrip("\n"), и removesuffix("\n"), — но ведут они себя по-разному, когда переносов два: rstrip снимет оба, removesuffix — ровно один. Здесь rstrip не опасен, потому что в наборе всего один символ и внутри данных он не встречается. Беда начинается ровно тогда, когда в аргумент strip-семейства попадает больше одного символа, как в ".txt".

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'

Заодно про соседний миф: сам по себе хвостовой \n разбор числа не ломает. float() отбрасывает пробельные символы по краям строки, поэтому float("77.59\n") спокойно вернёт 77.59. Строку "77,59\n" в число не превращает запятая, а не перенос. Все настоящие причины этой ошибки разобраны отдельно — could not convert string to float. Аккуратная чистка пробелов тренируется в бесплатной задаче «Аккуратные пробелы».

Частые ошибки

  1. rstrip(".txt") вместо removesuffix(".txt"). Классика: rstrip удалил буквы из имени файла, и в логах вместо report лежит repor. Фикс — removesuffix(".txt"), а если Python старше 3.9, то os.path.splitext(name)[0].
  2. split(".")[0] вместо rsplit(".", 1)[0]. На prices.2026.csv первый вариант отдаст prices и потеряет год, а на .gitignore — пустую строку.
  3. Жёсткий срез name[:-4]. Работает на .txt и .csv, ломается на .jpeg, .xlsx и файлах без расширения. Если очень нужен срез, считай длину: name[:-len(ext)].
  4. Забытый регистр. "IMG_2026.JPG".removesuffix(".jpg") вернёт строку без изменений. Сравнивай через name.lower().endswith(".jpg").
  5. Ожидание, что splitext снимет .tar.gz целиком. Не снимет — вернёт ('archive.tar', '.gz'). Для двойных расширений пиши суффикс явно.
  6. Путаница stem и with_suffix(""). Path("/home/anna/docs/report.txt").stem — это report без папок; если строка дальше используется как путь, нужен with_suffix("").
  7. Чтение списка имён из файла без encoding="utf-8". Имена приедут кракозябрами, и любые срезы будут работать не по тем символам. Разбор — в статье про кракозябры и UnicodeDecodeError.

Практика: почисти список имён файлов из выгрузки

Собери всё вместе — список имён из выгрузки чистится одной строкой:

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']

Прочитай вывод внимательно: archive.tar и .gitignore — это не ошибки, а ровно то поведение, которое мы разобрали выше. Если хочется закрепить строковые методы руками и с проверкой, начни с бесплатных задач по строкам — код запускается прямо в браузере.

Мини-резюме

  • rstrip(".txt") снимает набор символов ., t, x, а не окончание, поэтому report.txt превращается в repor.
  • Иногда rstrip угадывает верный ответ (photo.txt, отчёт.txt) — из-за этого баг долго остаётся незамеченным.
  • Расширение известно заранее — name.removesuffix(".txt"), Python 3.9 и новее.
  • Нужны и имя, и расширение — os.path.splitext(name); работаешь с путями — Path(name).stem.
  • Расширение неизвестно — name.rsplit(".", 1)[0]; split(".")[0] для этого не годится.
  • archive.tar.gz даёт ('archive.tar', '.gz'), у .gitignore расширения нет — оба случая нормальны.
  • Заменить расширение — Path(name).with_suffix(".csv"), а не склейка строк.

Закрепи на практике

Решай задачи в Python-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр