Toolmingo
Guides6 min read

Word Counter Guide: How to Count Words, Characters, and Lines in Text

Learn how word counters work, how to count words in JavaScript, Python, Go, and PHP, and the edge cases that trip up naive implementations.

Word count matters more than you might think. Blog editors set minimums for SEO. Academic submissions have strict limits. Contracts specify page lengths. Proofreaders bill by word count. And any time you paste text into a tool and click "Count Words", you're probably making an assumption about what "a word" actually means.

Here's how word counters work, how to build one in code, and the edge cases that break naive implementations.

What does "a word" mean?

Intuitively, a word is a sequence of characters separated by whitespace. That definition works for most English text — but it breaks down quickly:

Input Naive count Better count
"Hello, world!" 2 2
"it's a good day" 4 4
"café résumé" 2 2
"2024-01-15" 1 1 (date) or 3 (parts)?
"C++ rocks" 2 2
"北京 上海" 2 2 (space-separated CJK)
"你好世界" 1 4 (CJK, each char = word)
"" 0 0
" " 0 0

The most common definition — and the one used by most editors and writing tools — is: split on whitespace, discard empty tokens. That's it. Punctuation attached to words (commas, periods, quotes) stays attached and doesn't create separate tokens.

Counting words, characters, and lines

A full word counter typically tracks four metrics:

  • Words — whitespace-split tokens (empty tokens discarded)
  • Characters — total length including spaces and punctuation
  • Characters without spaces — total length excluding whitespace
  • Lines — number of newline-delimited lines (empty lines count)

JavaScript

function countText(text) {
  const words = text.trim() === '' ? 0 : text.trim().split(/\s+/).length;
  const chars = text.length;
  const charsNoSpaces = text.replace(/\s/g, '').length;
  const lines = text === '' ? 0 : text.split('\n').length;

  return { words, chars, charsNoSpaces, lines };
}

// Usage
const result = countText("Hello, world!\nThis is a test.");
// { words: 6, chars: 30, charsNoSpaces: 24, lines: 2 }

Key detail: text.trim().split(/\s+/) handles multiple spaces, tabs, and newlines between words. Without trim(), a leading space creates an empty first token and inflates the count by 1.

Python

def count_text(text: str) -> dict:
    words = len(text.split())          # str.split() handles all whitespace + strips
    chars = len(text)
    chars_no_spaces = len(text.replace(' ', '').replace('\t', '').replace('\n', ''))
    lines = len(text.splitlines()) if text else 0

    return {
        "words": words,
        "chars": chars,
        "chars_no_spaces": chars_no_spaces,
        "lines": lines,
    }

result = count_text("Hello, world!\nThis is a test.")
# {'words': 6, 'chars': 30, 'chars_no_spaces': 24, 'lines': 2}

str.split() with no arguments is Python's built-in word splitter — it handles any whitespace and strips leading/trailing space automatically. It's equivalent to text.strip().split(/\s+/) in JS.

Go

package main

import (
    "strings"
    "unicode"
)

type TextCount struct {
    Words        int
    Chars        int
    CharsNoSpace int
    Lines        int
}

func CountText(text string) TextCount {
    words := 0
    if strings.TrimSpace(text) != "" {
        words = len(strings.Fields(text)) // Fields splits on any whitespace
    }

    chars := len([]rune(text)) // rune count, not byte count (handles Unicode)
    charsNoSpace := 0
    for _, r := range text {
        if !unicode.IsSpace(r) {
            charsNoSpace++
        }
    }

    lines := 0
    if text != "" {
        lines = strings.Count(text, "\n") + 1
    }

    return TextCount{words, chars, charsNoSpace, lines}
}

Important: Use len([]rune(text)) for character count, not len(text). In Go, len() on a string returns the byte count, not the character count. For a string containing "café", len("café") is 5 (the é is two bytes in UTF-8), but len([]rune("café")) is 4.

PHP

function countText(string $text): array {
    $words = $text === '' ? 0 : str_word_count($text);
    $chars = mb_strlen($text);
    $charsNoSpaces = mb_strlen(preg_replace('/\s/u', '', $text));
    $lines = $text === '' ? 0 : count(explode("\n", $text));

    return [
        'words'          => $words,
        'chars'          => $chars,
        'chars_no_spaces' => $charsNoSpaces,
        'lines'          => $lines,
    ];
}

$result = countText("Hello, world!\nThis is a test.");
// ['words' => 6, 'chars' => 30, 'chars_no_spaces' => 24, 'lines' => 2]

Note on str_word_count(): This function uses locale-aware word splitting. Pass the text as the first argument with no format argument (or 0) to get the integer count. It handles punctuation attached to words correctly (treats "it's" as one word), but it can misbehave with non-Latin scripts — for those, fall back to preg_split('/\s+/u', trim($text)).

Word frequency counting

Beyond raw count, many tools show which words appear most often. This is useful for SEO keyword density checks and spotting repetition in your writing.

function wordFrequency(text) {
  const words = text.toLowerCase().match(/\b\w+\b/g) ?? [];
  const freq = {};
  for (const word of words) {
    freq[word] = (freq[word] ?? 0) + 1;
  }
  // Sort by frequency descending
  return Object.entries(freq).sort((a, b) => b[1] - a[1]);
}

wordFrequency("the cat sat on the mat the cat");
// [['the', 3], ['cat', 2], ['sat', 1], ['on', 1], ['mat', 1]]
from collections import Counter
import re

def word_frequency(text: str) -> list[tuple[str, int]]:
    words = re.findall(r'\b\w+\b', text.lower())
    return Counter(words).most_common()

Quick reference

Task JavaScript Python Go PHP
Count words text.trim().split(/\s+/).length len(text.split()) len(strings.Fields(text)) str_word_count($text)
Count chars text.length len(text) len([]rune(text)) mb_strlen($text)
Count chars (no spaces) text.replace(/\s/g,'').length len(text.split()) range + unicode.IsSpace mb_strlen(preg_replace('/\s/u','', $text))
Count lines text.split('\n').length len(text.splitlines()) strings.Count(text,"\n")+1 count(explode("\n",$text))
Word frequency match(/\b\w+\b/g) + Map Counter(re.findall(...)) map[string]int loop array_count_values

6 common mistakes

1. Counting empty strings as one word. "".split(" ") in JavaScript returns [""] — an array with one empty string. The fix: text.trim() === '' ? 0 : text.trim().split(/\s+/).length.

2. Using len(string) in Go for character count. In Go, len("café") returns 5 (bytes), not 4 (characters). Always use len([]rune(text)) or utf8.RuneCountInString(text) for character counts in Go.

3. Not handling multiple consecutive spaces. "hello world".split(" ") gives ["hello", "", "", "world"] — two phantom empty tokens. Use split(/\s+/) (regex) or split() (Python, which handles this automatically).

4. Forgetting Unicode in PHP. PHP's strlen() counts bytes, not characters. For any text that might contain accented characters or emoji, always use mb_strlen(). Similarly, use mb_substr() for slicing.

5. Treating line count as paragraph count. An empty line between paragraphs adds to line count. If you want paragraph count, split on \n\n (double newlines) and count non-empty chunks.

6. Assuming English word boundaries for CJK text. In Chinese, Japanese, and Korean, words are not space-separated. For accurate CJK word counts, you need a dedicated tokenizer (e.g., jieba for Chinese, MeCab for Japanese). A whitespace splitter will count entire sentences as single "words".

FAQ

Why do different tools give different word counts? They use different rules for punctuation, hyphenated words, and numbers. Microsoft Word counts "it's" as one word. Some tools count "2024-01-15" as one word; others split it on hyphens into three. There's no universal standard.

Does punctuation attached to a word count as part of it? In most tools, yes — "Hello," is treated as one word. The punctuation doesn't create a separate token. Only whitespace (spaces, tabs, newlines) creates word boundaries.

What's the difference between characters and bytes? A character is a Unicode code point (a letter, digit, or symbol). A byte is a unit of storage. ASCII characters (A–Z, 0–9, common punctuation) are one byte each in UTF-8. Accented characters like é are two bytes. Emoji like 😀 are four bytes. Users think in characters; your database and strlen() think in bytes.

How do I count words in a file instead of a string? In Python: len(open('file.txt').read().split()). In Go: read with os.ReadFile, convert to string, then strings.Fields(). In PHP: str_word_count(file_get_contents('file.txt')). For very large files (>100 MB), stream line by line to avoid loading the entire file into memory.

How do I exclude stop words from frequency counts? Define a set of common words to ignore (["the", "a", "is", "in", ...]) and filter them out before counting: words.filter(w => !stopWords.has(w)) in JavaScript, or [w for w in words if w not in stop_words] in Python.

What's the fastest way to count words in a very large document? Stream the file line by line and accumulate counts. In Python, wc -w (the Unix command) is the fastest option for multi-gigabyte files — it's implemented in C and reads the file in buffered chunks. For cross-platform programmatic use, process line by line using a buffered reader.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools