Skip to content

Enter and Shift+Enter in a Word document

Level: 201 · for anyone who has copied text out of a Word document into a field, a file or an interface

One line: In a .docx neither Enter is a character — Shift+Enter is the seven ASCII bytes <w:br/> and Enter is only the place a <w:p> element closes, inside UTF-8 XML inside a ZIP — and they become the control characters 0b and 0d only when text leaves Word, which is why none of your line tools can see them.

The question

A Word document on a Windows PC, with formatting marks switched on, shows four marks down the left margin and nothing else:

↵
↵

What encoding is this document, and what bytes are behind those four Enters?

It looks like four Enters. It is two paragraphs, each holding a single line break. The arrow is a manual line break (Shift+Enter): it ends a line without ending the paragraph, so the paragraph's own mark drops to the next line. The pilcrow is the paragraph mark (Enter). Word draws the two differently because they are two different things, and in the file the difference is bigger than it looks on screen: one of them is an element of its own and the other has no bytes at all.

Three layers, and the bytes are in the middle one

The file is a ZIP. The first four bytes of any .docx are 50 4b 03 04, PK, the signature of a ZIP entry, and the text lives in a member called word/document.xml, compressed with deflate. That has a practical consequence before anything else: searching the .docx itself finds nothing. Section 1 of both runs below searches the file for <w:br/> and finds nothing, while the part holds two. Every question about the bytes has to be asked of the part after you unzip it.

The part is UTF-8 XML, and it says so on line 1. A .docx has no code-page setting to go looking for. The first line of word/document.xml is <?xml version="1.0" encoding="UTF-8" standalone="yes"?>, and é inside it is c3 a9. So "which encoding is my Word document?" has one answer, written in the file. The older binary .doc format is a different story, and this page does not cover it beyond one sentence further down.

Inside the XML the two Enters are structure, not characters:

On screen Key In word/document.xml Bytes
↵ Shift+Enter the element <w:br/>, inside a run <w:r> 3c 77 3a 62 72 2f 3e
Enter the end of a paragraph, </w:p> none of its own: 3c 2f 77 3a 70 3e is the closing tag
(a page break) Ctrl+Enter <w:br w:type="page"/> 21 bytes, still no control character

So the four marks in the screenshot are, in full, <w:p><w:r><w:br/></w:r></w:p> twice. Count the line-ending bytes in that part and there is exactly one CR and one LF, both on line 1, straight after the XML declaration. The four Enters put no 0a, 0b or 0d into the file.

Where they become characters

They turn into bytes only on the way out of Word. Word's object model — what a VBA macro reads through Range.Text — represents a manual line break as Chr(11), vertical tab, 0b, and a paragraph mark as Chr(13), carriage return, 0d. Those are the codes Word's own Find box accepts as ^11 and ^13, beside the named ^l and ^p. The screenshot's document reads out of Word as 0b 0d 0b 0d.

Neither of those is 0a, and that is where the trouble starts. A 0d with no 0a after it is the classic Mac line ending, which CRLF vs LF takes apart. And 0b is the control character whose row in that page's table reads nobody uses it. Word uses it every time somebody presses Shift+Enter, and the tools do not agree about it: wc -l does not count it, Python's open() leaves it inside the line, and str.splitlines() splits on it. Section 5 of the Python run counts one piece of Word text four ways and gets 0, 2, 1 and 4. A value copied out of a document typed with Shift+Enter carries a byte that no check for CR or LF will ever find.

The same two codes, 0d and 0b, are what the old binary .doc format stores in its text stream. That is the one sentence promised above.

(Not machine-checked. CI cannot run Word, so the Chr(11) / Chr(13) mapping, the Find codes, the keys, the glyphs Word draws and the .doc storage are all prose. The Python example reproduces the mapping but cannot confirm it; the second prompt under Try it confirms it on your own machine.)

Real Word files, measured

The examples build their document the way Word lays one out. That is a claim about Word, so here it is checked against Word's own output. These are all the .docx files on one Mac whose docProps/app.xml names Microsoft Word as the application, with duplicate copies counted once. The file does not record whether Word for Windows or Word for Mac wrote it.

Measured 2026-09-16 — 13 distinct .docx files written by Microsoft Office Word, AppVersion 16.0000, on one Mac. Not machine-checked: the files are not in the repo and CI has no Word.
start with 50 4b 03 04, [Content_Types].xml first, document.xml deflated   13 of 13
document.xml line 1 is the UTF-8 declaration, then 0d 0a at offsets 55-56  13 of 13
  ...and that is the only 0d and the only 0a in the part                  13 of 13
0b bytes in document.xml, all files                                        0
0c bytes in document.xml, all files                                        0
<w:br/>                    (manual line break)                             80
<w:br w:type="page"/>      (page break)                                    11
<w:cr/>                                                                    0
'<w:br' found by searching the raw .docx bytes                             0
paragraphs, all files                                                      3313
  ...holding one run with one <w:br/> and no text: the screenshot's shape  6

Every line matches what the examples assume, and the last one shows the screenshot's shape is ordinary. Word writes those six paragraphs with property elements (<w:pPr>, <w:rPr>) and id attributes that the examples leave out. None of that adds a byte below 0x20.

In Python

Verified output of word_paragraph_and_line_break_py.py — regenerated by tools/run_examples.py, never hand-typed.

1. THE FILE IS A ZIP, SO THE MARKUP IS NOT IN IT AS BYTES
------------------------------------------------------------------------
   first four bytes of the .docx   50 4b 03 04   'PK', a ZIP local header
   members                         [Content_Types].xml, _rels/.rels, word/document.xml
   word/document.xml               239 bytes of XML, method 8 (deflate)

   b'<w:br/>' in the .docx itself       False
   b'<w:br/>' in word/document.xml      True

   Searching the .docx for the markup finds nothing, because what is
   on disk is deflate output. Every question on this page is asked of
   the part after unzipping it, never of the file you double-click.

2. THE ENCODING IS NOT A SETTING: LINE 1 OF THE PART SAYS UTF-8
------------------------------------------------------------------------
   line 1    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
   then      0d 0a   CR LF, at offsets 55 and 56

   One paragraph holding café, to see a non-ASCII letter inside:
     the bytes of café in the part     63 61 66 c3 a9
     ElementTree, which obeys line 1   'café'
     the same bytes read as cp1252     'café'

   é is c3 a9: UTF-8. A .docx has no code page to choose, so 'which
   encoding is my Word document?' has one answer, and it is written on
   the first line of every part.

3. THE FOUR MARKS, AS BYTES
------------------------------------------------------------------------
   <w:p>        3c 77 3a 70 3e
     <w:r>      3c 77 3a 72 3e
       <w:br/>  3c 77 3a 62 72 2f 3e  <- the arrow: Shift+Enter
     </w:r>     3c 2f 77 3a 72 3e
   </w:p>       3c 2f 77 3a 70 3e     <- the pilcrow: Enter
   ...and the second paragraph is the same five tags again.

   in the whole part:  <w:br/> 2 times,  </w:p> 2 times
   bytes 0a LF         1   at offset 56
   bytes 0b VT         0
   bytes 0d CR         1   at offset 55

   The only CR and the only LF in the part are the pair after the
   declaration on line 1. The four marks on screen put no line-ending
   byte into the file at all: the arrow is seven bytes of ASCII markup
   and the pilcrow is the end of an element.

   A page break is the same element with an attribute:
     <w:br w:type="page"/>   21 bytes, still no control character

4. DRAWING IT BACK FROM THE XML
------------------------------------------------------------------------
   Walk the part, draw an arrow per <w:br/> and a pilcrow where each
   <w:p> closes, and the screenshot comes back:

     ↵
     ↵

   Two paragraphs, not four. The arrow is on its own line because a
   line break ends the line it is on; the pilcrow then marks where the
   paragraph ends, and the paragraph has nothing else in it.

5. ON THE WAY OUT OF WORD THEY BECOME 0B AND 0D
------------------------------------------------------------------------
   the screenshot, as a macro would read it   0b 0d 0b 0d
   with words in it                           'one\x0btwo\rthree\x0bfour\r'

   That mapping -- Chr(11) for the arrow, Chr(13) for the pilcrow -- is
   the one Word's object model uses. This program reproduces it; it
   cannot run Word. Now count the lines in that text four ways:

     0a bytes, which is all wc -l counts    0
     open(), default newline=None           2
     text.split('\n')                       1
     text.splitlines()                      4

   Line 1 from open() is 'one\x0btwo\n': the CR became a line end and the
   VT stayed inside the line. So open() says two lines, splitlines()
   says four, and a value copied out of line 1 carries a control
   character that no check for CR or LF will ever find.

6. SO WHERE DOES EACH ENTER LIVE?
------------------------------------------------------------------------
                          Shift+Enter              Enter
   on screen              ↵                        ¶
   in the .docx           <w:br/>                  the end of <w:p>
     as bytes             3c 77 3a 62 72 2f 3e     none of its own
   out of Word            0b  VT                   0d  CR

   And neither one is 0a, LF -- the only byte most line tools count.

In the terminal

Verified output of word_paragraph_and_line_break_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

1. GREP THE .DOCX AND YOU FIND NOTHING
------------------------------------------------------------------------

   head -c 4 demo.docx | xxd -p              504b0304
   grep -a -c "<w:br/>" demo.docx            0
   unzip -p ... | grep -o "<w:br/>" | count  2

   504b0304 is PK, a ZIP entry. The markup is inside a deflated member,
   so a search of the file on disk reports zero while the part holds
   two. unzip -p writes a member to stdout, and every command below
   starts from that.

2. LINE 1 OF THE PART IS THE ENCODING
------------------------------------------------------------------------

   unzip -p demo.docx word/document.xml | head -1 | cat -vet

     <?xml version="1.0" encoding="UTF-8" standalone="yes"?>^M$

   UTF-8, declared, and the ^M$ at the end is the CR LF Word writes
   after the declaration. It is the first line ending in the part and,
   as section 4 counts, the last.

3. THE TWO MARKS, AS BYTES
------------------------------------------------------------------------

   the arrow, Shift+Enter:
     00000000: 3c77 3a62 722f 3e                        <w:br/>

   the pilcrow, Enter -- no byte of its own, only the end of <w:p>:
     00000000: 3c2f 773a 703e                           </w:p>

   Seven bytes and six bytes of plain ASCII markup. Nothing in either
   is below 0x20.

4. EVERY LINE-ENDING BYTE IN THE PART
------------------------------------------------------------------------

   unzip -p ... | tr -dc "\012\013\015" | xxd -p   0d0a

   One CR and one LF, both from line 1. Four marks on screen, and not
   one of them left a 0a, 0b or 0d in the file.

If you are coming from Python or ABAP

Python. Two stdlib modules, zipfile and xml.etree.ElementTree, are the whole toolkit, and word_text() in the example reproduces the object model's reading of a part in about a dozen lines. ElementTree.fromstring takes the part as bytes and obeys the declaration, so there is nothing to decode first — and decoding it yourself with the wrong table is the one way left to get é wrong, as section 2 shows. If you use a library such as python-docx instead, it makes its own choice about what <w:br/> becomes, and that choice need not be Word's \x0b, so print a repr() before trusting it. And pick the splitter deliberately: str.splitlines() treats \x0b as a line boundary and open()'s universal newlines do not, so the same text is four lines to one of them and two to the other.

ABAP. A .docx reaches ABAP as an xstring, from an upload or an attachment, and it is a ZIP there too: cl_abap_zip loads the container and gets the member word/document.xml by name. What comes out is still an xstring of UTF-8. Convert it to a string with the code page stated as UTF-8 (cl_abap_conv_codepage=>create_in( ) on newer releases, cl_abap_conv_in_ce on older ones) — on the newer class that is also the default of codepage, not the system code page ↗ — or skip the conversion and hand the bytes straight to the XML parser. In the parsed document the two Enters are exactly what this page shows, an element and an element's end. They become characters only if your code writes them in, and choosing which character is the real decision: cl_abap_char_utilities=>vertical_tab is Word's own choice for Shift+Enter and almost never what a receiving field wants, while =>newline or =>cr_lf depends on who reads the result, as on CRLF vs LF. Check every class and constant name on your release. (Not machine-checked — CI cannot run ABAP.)

Try it

cd 07_Real_Data/word_paragraph_and_line_break/examples
python3 word_paragraph_and_line_break_py.py
bash word_paragraph_and_line_break_sh.sh
  1. On the PC that has the document. Switch formatting marks on (Show/Hide ¶, Ctrl+Shift+8) and count the arrows and pilcrows first. Then, in PowerShell, Copy-Item .\yours.docx .\yours.zip (Windows PowerShell 5.1's Expand-Archive insists on a .zip name), then Expand-Archive .\yours.zip .\yours_x, then Format-Hex .\yours_x\word\document.xml | Select-Object -First 4. Find bytes 55 and 56. Then count (Select-String -Path .\yours_x\word\document.xml -Pattern '<w:br/>' -AllMatches).Matches.Count and say beforehand what it should equal.
  2. The one claim this page could not run. In Word, Alt+F11, then Ctrl+G for the Immediate window, and run ?Replace(Replace(ActiveDocument.Content.Text, Chr(11), "[0B]"), Chr(13), "[0D]"). Write down what you expect from the marks on screen, then compare.
  3. The empty grep. On a Mac or Linux, run grep -c '<w:br/>' yours.docx, then unzip -p yours.docx word/document.xml | grep -o '<w:br/>' | grep -c ., on a document you know has Shift+Enter in it. Then try it on a .xlsx or .pptx, which are ZIPs of XML too.
  4. A producer fingerprint. Run unzip -p some.docx word/document.xml | head -1 | cat -vet on a .docx that Word did not write: a Google Docs export, a LibreOffice save, a report a program generates. Is the ^M after the declaration there? What does its docProps/app.xml name?
  5. What the clipboard does. Copy a paragraph containing a Shift+Enter out of Word, paste it into a plain-text editor, save, and xxd the result. Did the arrow survive as 0b, turn into a line ending, or disappear? Then paste the same thing into the SAP field or spreadsheet cell it was really headed for.

See also