How to use
- Paste the list, one entry per line.
- Tick Ignore letter case if
Appleandappleshould count as the same line; leave Trim spaces before comparing on so stray spaces do not create false uniques. - Choose which copy to keep, First occurrence or Last occurrence, and whether to sort the result.
- Read the counter: it shows how many unique lines remain and how many were removed.
- Copy to clipboard or Download .txt.
When duplicate lines sneak in
Merged spreadsheets, combined mailing lists, log files tailed twice, and copy-pasted data from multiple sources all tend to accumulate exact repeats. A quick visual scan rarely catches every one, especially once a list runs past a screen's worth of lines. This tool compares every line against every other line and keeps just one copy of each.
By default it preserves your original ordering (the first time a line appears it stays where it was, rather than the whole list being resorted), so a deduplicated list is a strict subset of what you pasted in, easy to diff against the original if you need to check what was removed. If the values are on one line separated by commas rather than one per line, split them first with the list to column converter.
Example
Input:
apple
banana
Apple
cherry
banana
dateWith the default options, Apple and apple are different lines, so the result is:
apple
banana
Apple
cherry
dateWith Ignore letter case on, Apple is treated as a duplicate of apple and dropped:
apple
banana
cherry
dateThe counter reads “4 lines · 2 removed” for the second case, 5 lines · 1 removed for the first.
Options explained
- Ignore letter case compares lowercased copies of each line. The surviving line keeps its original casing; whichever copy came first (or last) wins.
- Trim spaces before comparing removes leading and trailing spaces and tabs from every line, in the output as well as for the comparison.
- Remove blank lines drops empty lines entirely. Untick it and blank lines are treated like any other line: they are all duplicates of each other, so exactly one survives.
- Keep only lines that had duplicates inverts the filter: the output is one copy of each line that appeared two or more times, and nothing else.
a, b, a, c, cgivesa, c; if nothing was repeated, the result is empty. - Keep: First occurrence / Last occurrence chooses which copy survives. With Last, the output order follows the last occurrences:
a, b, a, cbecomesb, a, c. - Sort result: Keep original order / A → Z / Z → A sorts the deduplicated output in natural, case-insensitive order (
x2beforex10,Abeforeb).
Edge cases and common errors
- Only exact text matches.
cafétyped with a precomposed é andcafétyped with a combining accent look identical but are different strings, and both survive. Normalise upstream if that matters, for example withunicodedata.normalize("NFC", s)in Python. - Trailing spaces with trimming off make
aandadifferent lines:a, [space]a[space], agives 2 unique lines instead of 1. - Windows (
\r\n) and old-Mac (\r) line endings are recognised; the output always uses\n. - Blank lines are not counted as “removed”. The output counter compares non-blank lines only, so five pasted lines with two blanks reads “3 lines” with nothing removed. The input counter shows the raw line total if you need to reconcile.
- Empty input gives an empty output and a count of 0. No error is raised.
Do the same thing in code
On the command line, each of these keeps one copy of every line:
sort -u file.txt # sorted, unique lines
awk '!seen[$0]++' file.txt # unique lines, original order preserved
Get-Content file.txt | Select-Object -Unique # PowerShell, original ordersort -u and awk are case-sensitive and do not trim; add -f to sort for case-folding.
In Python, a dictionary remembers the order its keys were added in, so two lines keep the first copy of each line in order:
lines = [line.strip() for line in text.split("\n")]
unique = list(dict.fromkeys(line for line in lines if line))Use split("\n") rather than splitlines() here: splitlines() also breaks lines at form feeds, U+0085 and U+2028, which the tool leaves inside the line. The script below does everything the tool does (case-insensitive matching, keeping the last copy, repeated lines only, sorting) and prints the same counter. It needs Python 3.9 or later and only the standard library.
remove_duplicate_lines.py#!/usr/bin/env python3
"""Remove duplicate lines from a text file or a pasted list, keeping the first (or last) copy.
Does what the Remove Duplicate Lines tool at
https://parttoolhub.com/tools/remove-duplicate-lines does, with the same options:
python remove_duplicate_lines.py emails.txt > unique.txt
python remove_duplicate_lines.py --ignore-case --sort az < export.txt
python remove_duplicate_lines.py --only-duplicates log.txt
python remove_duplicate_lines.py --help
Or import it: from remove_duplicate_lines import dedupe_lines
The count line ("N lines, M removed") goes to standard error, so a redirect keeps only the list.
Sorting reads digit runs as numbers and ignores case and accents (x2 before x10, A before b),
and orders punctuation the way a browser with an English locale does. Other languages' browsers
sort their own alphabets by their own rules; the order of the lines you keep never depends on it.
Python 3.9 or later, standard library only.
"""
import argparse
import re
import sys
import unicodedata
from collections import namedtuple
# What JavaScript's String.trim() removes. Python's str.strip() uses a different set (it drops
# U+001C to U+001F and U+0085, and keeps U+FEFF), so the set is spelled out here.
JS_WHITESPACE = (
"\t\n\v\f\r \u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a"
"\u2028\u2029\u202f\u205f\u3000\ufeff"
)
LINE_BREAK = re.compile(r"\r\n|\r|\n")
DIGITS = re.compile(r"[0-9]+")
# The order an English-locale browser sorts whitespace and ASCII punctuation in, all of it before
# digits and letters. Every kind of space (ordinary, no-break, ideographic ...) counts as the same
# one, and control and format characters (zero-width space, BOM) count for nothing.
SPACES = " \u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000"
SORT_ORDER = ["\t", "\n", "\v", "\f", "\r", "\u0085", "\u2028", "\u2029", SPACES, *r"""_-,;:!?.'"()[]{}@*/\&#%`^+<=>|~$"""]
PLACE = {ch: place for place, group in enumerate(SORT_ORDER) for ch in group}
Result = namedtuple("Result", "output total unique removed")
def _sort_key(line):
"""One tuple per character, one per run of digits, so lines compare the way a person reads them."""
key = []
i = 0
while i < len(line):
digits = DIGITS.match(line, i)
if digits:
run = digits.group().lstrip("0") # 007 and 7 are the same number
key.append((1, len(run), run))
i = digits.end()
continue
ch = line[i]
i += 1
category = unicodedata.category(ch)
if category == "Cf" or (category == "Cc" and ch not in PLACE):
continue
if ch.isalpha():
# Accents and case do not matter; a letter like ß counts as the two letters ss.
folded = unicodedata.normalize("NFD", unicodedata.normalize("NFD", ch).casefold())
key.extend((2, c, "") for c in folded if not unicodedata.combining(c))
else:
key.append((0, PLACE.get(ch, 1000 + ord(ch)), ""))
return key
def dedupe_lines(text, ignore_case=False, trim=True, remove_blank=True, keep="first",
sort="none", only_duplicates=False):
"""Return Result(output, total, unique, removed). keep: "first" or "last". sort: "none", "az" or "za".
total counts every line of the input, blank ones too. unique counts the distinct lines that
were compared; removed is how many compared lines were repeats.
"""
if not text:
return Result("", 0, 0, 0)
lines = LINE_BREAK.split(text)
total = len(lines)
if trim:
lines = [line.strip(JS_WHITESPACE) for line in lines]
if remove_blank:
lines = [line for line in lines if line.strip(JS_WHITESPACE)]
def key(line):
return line.lower() if ignore_case else line
counts = {}
for line in lines:
counts[key(line)] = counts.get(key(line), 0) + 1
ordered = lines[::-1] if keep == "last" else lines
seen = set()
result = []
for line in ordered:
k = key(line)
if k in seen:
continue
seen.add(k)
if not only_duplicates or counts[k] > 1:
result.append(line)
if keep == "last":
result.reverse()
if sort == "az":
result.sort(key=_sort_key)
elif sort == "za":
result.sort(key=_sort_key, reverse=True)
unique = len(seen)
return Result("\n".join(result), total, unique, len(lines) - unique)
def main():
p = argparse.ArgumentParser(description="Remove duplicate lines, keeping one copy of each.")
p.add_argument("file", nargs="?", help="file to read (default: standard input)")
p.add_argument("--ignore-case", action="store_true", help="treat Apple and apple as the same line")
p.add_argument("--no-trim", action="store_true", help="compare lines exactly, spaces included")
p.add_argument("--keep-blank", action="store_true", help="keep blank lines (one survives)")
p.add_argument("--keep", choices=["first", "last"], default="first", help="which copy survives")
p.add_argument("--sort", choices=["none", "az", "za"], default="none", help="sort the result")
p.add_argument("--only-duplicates", action="store_true", help="print one copy of each repeated line, nothing else")
a = p.parse_args()
if a.file:
with open(a.file, encoding="utf-8", newline="") as f:
text = f.read()
else:
sys.stdin.reconfigure(encoding="utf-8")
text = sys.stdin.read()
r = dedupe_lines(
text,
ignore_case=a.ignore_case,
trim=not a.no_trim,
remove_blank=not a.keep_blank,
keep=a.keep,
sort=a.sort,
only_duplicates=a.only_duplicates,
)
sys.stdout.reconfigure(encoding="utf-8")
if r.output:
print(r.output)
print(f"{r.unique} lines, {r.removed} removed", file=sys.stderr)
if __name__ == "__main__":
main()Run it on a file or pipe a list through it. The list goes to standard output and the counter to standard error, so> saves only the list:
python remove_duplicate_lines.py emails.txt > unique.txt
python remove_duplicate_lines.py --ignore-case --keep last --sort az < export.txt
python remove_duplicate_lines.py --only-duplicates log.txtFrom your own code: from remove_duplicate_lines import dedupe_lines, thendedupe_lines(text, ignore_case=True) returns the output with the three counts.
The site's test suite runs this script and the tool on the same inputs (the tool's own test cases plus about fifteen hundred generated lists, padded with every kind of space, under random option combinations) and fails on any difference in the output or the counts. With the default comparison and no sorting, the script's result must also equal Python's own dict.fromkeys. Sorting follows an English-locale browser; other languages sort their own alphabets differently, but the lines you keep are the same.