![pilcrow](https://cdn.slatesource.com/a/e/8/ae86e536-aa91-47b1-aecd-72709410bc61.webp)

# pilcrow

- [Made in Slatesource](https://slatesource.com/@steph/pilcrow)
- By [Steph](https://slatesource.com/@steph)
- Created on Aug 5, 2026

## pilcrowpilcrow

PastePaste from WordWord, a PDFPDF, an emailemail clientclient or a CMSCMS and you getget sentencessentences brokenbroken by hardhard lineline breaksbreaks, runsruns of doubledouble spacesspaces, non-breakingnon-breaking spacesspaces where they do not belongbelong, and smartsmart quotesquotes that survivedsurvived threethree roundround tripstrips and then did not. This librarylibrary turnsturns that into cleanclean paragraphsparagraphs. It does not do anythinganything elseelse, and that is the wholewhole designdesign.

## Who This Is For

You maintainmaintain somethingsomething that acceptsaccepts texttext a humanhuman pastedpasted in from somewheresomewhere elseelse. A CMSCMS fieldfield, a contactcontact formform, a note-takingnote-taking appapp, an importimport pipelinepipeline. You have writtenwritten the samesame threethree regexesregexes at leastleast twicetwice. They were subtlysubtly wrongwrong bothboth timestimes. The bugbug that mademade you writewrite them the secondsecond timetime camecame from a FrenchFrench documentdocument, where the non-breakingnon-breaking spacespace beforebefore a coloncolon is correctcorrect and mustmust be keptkept.

Install

0%

pip install pilcrow

Python 3.9 or newer

No dependencies, and none planned. There is nothing else to install

Optional: pip install "pilcrow\[cli\]" if you want the pilcrow command

## The OneOne ThingThing It Does

normalisenormalise() takestakes a stringstring and returnsreturns a stringstring. It joinsjoins lineslines brokenbroken mid-sentencemid-sentence, keepskeeps the breaksbreaks that were deliberatedeliberate, collapsescollapses whitespacewhitespace runsruns withoutwithout touchingtouching indentationindentation insideinside fencedfenced blocksblocks, and leavesleaves typographictypographic characterscharacters alonealone unlessunless you askask otherwiseotherwise.

## How It DecidesDecides

A breakbreak is deliberatedeliberate if the lineline beforebefore it endsends in sentencesentence punctuationpunctuation, is shortershorter than the runningrunning medianmedian lineline lengthlength, or startsstarts a listlist. ThreeThree rulesrules, in that orderorder. They are wrongwrong for poetrypoetry, and poetrypoetry is out of scopescope in the READMEREADME ratherrather than in a heuristicheuristic.

Public functions

3

Runtime dependencies

0

Python versions in CI

3.9 to 3.13

Median first response on issues

4 days

Contributors

11

## What It Will Not Do

It does not detectdetect languagelanguage, spellcheckspellcheck, stripstrip HTMLHTML, convertconvert MarkdownMarkdown or guessguess what you meantmeant. EachEach of those has been requestedrequested and declineddeclined, and the reasoningreasoning sitssits in the issueissue threadthread ratherrather than behindbehind a wontfixwontfix labellabel. If you needneed HTMLHTML strippingstripping, useuse somethingsomething that actuallyactually parsesparses HTMLHTML. A regexregex that halfhalf understandsunderstands HTMLHTML is how the secondsecond bugbug on this pagepage startedstarted.

> I deleted sixty lines of accumulated regex and replaced them with one function call. The French quotation marks finally survived the round trip.

Your first run

0%

Call normalise(text) and read the output before you configure anything

If a break you wanted was removed, pass keep\_breaks=True

If a non-breaking space you needed was removed, pass locale="fr"

Run pilcrow --diff against a real file from your corpus, not a synthetic one

Only then decide whether you need the other two functions

## Where To Go NextNext

The READMEREADME is the documentationdocumentation. ThereThere is no separateseparate docsdocs sitesite, becausebecause therethere is not enoughenough surfacesurface areaarea to justifyjustify oneone and a docsdocs sitesite that lagslags the READMEREADME is worseworse than nonenone at all. The APIAPI referencereference fitsfits on oneone pagepage. If somethingsomething readsreads as unclearunclear, that is a documentationdocumentation bugbug and it is worthworth an issueissue.

[pilcrow on PyPI](https://pypi.org/project/pilcrow/?utm_source=slatesource)

[Source and issue tracker](https://github.com/ellamaki/pilcrow?utm_source=slatesource)