podhmo: ```python
import numpy as np

np.arange(9).reshape((3, 3))  # =>
```

to

```python
import numpy as np

np.arange(9).reshape((3, 3))  # => multi-line..
# array([[0, 1, 2],
#        [3, 4, 5],
#        [6, 7, 8]])
# ..multi-line
```
diff --git a/README.rst b/README.rst
index 99e9046..3b44537 100644
--- a/README.rst
+++ b/README.rst
@@ -52,3 +52,16 @@ code.py
   # -- stdout --------------------
   # >> hello
   # >> bye
+
+multi-line output
+
+.. code:: python
+
+  import numpy as np
+
+  np.arange(9).reshape((3, 3))  # => multi-line..
+  # array([[0, 1, 2],
+  #        [3, 4, 5],
+  #        [6, 7, 8]])
+  # ..multi-line
+
diff --git a/examples/03loop.py b/examples/03loop.py
index 798bca7..bf25e36 100644
--- a/examples/03loop.py
+++ b/examples/03loop.py
@@ -9,7 +9,7 @@ import itertools
 
 itertools.combinations(
     [1, 2, 3, 4, 5], 2
-)  # => <itertools.combinations object at 0x7fe9f8135ef8>
+)  # => <itertools.combinations object at 0x7f4d6f51c4a8>
 list(
     itertools.combinations([1, 2, 3, 4, 5], 3)
 )  # => [(1, 2, 3), (1, 2, 4), (1, 2, 5), (1, 3, 4), (1, 3, 5), (1, 4, 5), (2, 3, 4), (2, 3, 5), (2, 4, 5), (3, 4, 5)]
diff --git a/examples/04multi-line.py b/examples/04multi-line.py
new file mode 100644
index 0000000..ce9369c
--- /dev/null
+++ b/examples/04multi-line.py
@@ -0,0 +1,7 @@
+import numpy as np
+
+np.arange(9).reshape((3, 3))  # => multi-line..
+# array([[0, 1, 2],
+#        [3, 4, 5],
+#        [6, 7, 8]])
+# ..multi-line
diff --git a/pycomment/capture.py b/pycomment/capture.py
index e4b710a..baf988d 100644
--- a/pycomment/capture.py
+++ b/pycomment/capture.py
@@ -14,12 +14,28 @@ def capture(code: str, *, g: t.Optional[t.IO] = None):
         exec(code, g)
 
     result_map = {}
-    stdout_outputs = []
-    for line in o.getvalue().splitlines():
-        if line.startswith(SEP_MARKER) and line.endswith(SEP_MARKER):
-            line = line.strip(SEP_MARKER)
-            lineno, line = line.split(":", 1)
-            result_map[lineno] = line
+    rest = []
+
+    reading = False
+    buf = None
+    lineno = None
+
+    lines = o.getvalue().splitlines()
+    for line in lines:
+        if reading:
+            if line.endswith(SEP_MARKER):
+                reading = False
+                buf.append(line.rstrip(SEP_MARKER))
+                result_map[lineno] = buf
+            else:
+                buf.append(line)
+        elif line.startswith(SEP_MARKER):
+            reading = True
+            lineno, line = line.lstrip(SEP_MARKER).split(":", 1)
+            buf = [line.rstrip(SEP_MARKER)]
+            if line.endswith(SEP_MARKER):
+                reading = False
+                result_map[lineno] = buf
         else:
-            stdout_outputs.append(line)
-    return CaptureResult(comments=result_map, stdout=stdout_outputs)
+            rest.append(line)
+    return CaptureResult(comments=result_map, stdout=rest)
diff --git a/pycomment/emit.py b/pycomment/emit.py
index 0c4e36c..e236056 100644
--- a/pycomment/emit.py
+++ b/pycomment/emit.py
@@ -1,19 +1,21 @@
 import typing as t
 import sys
 import re
+from .langhelpers import PushBackIterator
 from . import COMMENT_MARKER, STDOUT_HEADER_MARKER
 
 
 def emit(
     rf: t.Iterable[str],
     *,
-    result_map: t.Dict[str, str],
+    result_map: t.Dict[str, t.List[str]],
     rest: t.List[str],
     out: t.IO = sys.stdout
 ) -> None:
     i = 0  # xxx
     rx = re.compile(COMMENT_MARKER + ".*$")
-    for lineno, line in enumerate(rf, 1):
+    itr = PushBackIterator(enumerate(rf, 1))
+    for lineno, line in itr:
         if line.rstrip() == STDOUT_HEADER_MARKER:
             break
 
@@ -22,7 +24,26 @@ def emit(
         if m is None or k not in result_map:
             print(line, end="", file=out)
         else:
-            print(line[: m.start()] + COMMENT_MARKER, result_map[k], file=out)
+            comments = result_map[k]
+            if len(comments) == 1:
+                print(line[: m.start()] + COMMENT_MARKER, comments[0], file=out)
+            else:
+                print(
+                    line[: m.start()] + COMMENT_MARKER,
+                    "multi-line..",
+                    file=out,
+                )
+
+                for lineno, line in itr:
+                    if not line.startswith("# "):
+                        itr.pushback((lineno, line))
+                        break
+                    if line.startswith("# ..multi-line"):
+                        break
+
+                for comment in comments:
+                    print("#", comment, file=out)
+                print("# ..multi-line", file=out)
             i += 1
 
     if rest:
diff --git a/pycomment/langhelpers.py b/pycomment/langhelpers.py
index 34db2d2..64cc10e 100644
--- a/pycomment/langhelpers.py
+++ b/pycomment/langhelpers.py
@@ -1,3 +1,5 @@
+import typing as t
+
 # stolen from pyramid
 class reify:
     """cached property"""
@@ -15,3 +17,17 @@ class reify:
         val = self.wrapped(inst)
         setattr(inst, self.wrapped.__name__, val)
         return val
+
+
+class PushBackIterator:
+    def __init__(self, itr) -> None:
+        self.itr = iter(itr)
+
+    def pushback(self, x: str) -> None:
+        self.itr = itertools.chain([x], self.itr)
+
+    def __next__(self) -> str:
+        return next(self.itr)
+
+    def __iter__(self) -> t.Iterator[str]:
+        return self

multiline output support
```python
import numpy as np

print(np.arange(9).reshape((3, 3)))
print(np.arange(9).reshape((3, 3)))
```

output is

```
[[0 1 2]
 [3 4 5]
 [6 7 8]]
[[0 1 2]
 [3 4 5]
 [6 7 8]]
```

but below code with `pycomment --inplace <filename>.py`

```python
import numpy as np

np.arange(9).reshape((3, 3))  # =>
np.arange(9).reshape((3, 3))  # =>
# -- stdout --------------------
# >> ZZ󰀀ZZ4:array([[0, 1, 2],
# >>        [3, 4, 5],
# >>        [6, 7, 8]])ZZ󰀀ZZ
# >> ZZ󰀀ZZ3:array([[0, 1, 2],
# >>        [3, 4, 5],
# >>        [6, 7, 8]])ZZ󰀀ZZ
# >> ZZ󰀀ZZ4:array([[0, 1, 2],
# >>        [3, 4, 5],
# >>        [6, 7, 8]])ZZ󰀀ZZ
```
diff --git a/pycomment/tests/_fakearray.py b/pycomment/tests/_fakearray.py
new file mode 100644
index 0000000..01ab525
--- /dev/null
+++ b/pycomment/tests/_fakearray.py
@@ -0,0 +1,42 @@
+class FakeArray:
+    def __init__(self, xs, *, shape=None):
+        self.xs = xs
+        self.shape = shape or len(xs)
+
+    def reshape(self, shape):
+        return FakeArray(_reshape(self.flatten(), shape))
+
+    def flatten(self, xs=None):
+        for x in xs or self.xs:
+            if isinstance(x, (list, tuple)):
+                yield from self.flatten(x)
+            else:
+                yield x
+
+    def __repr__(self):
+        from io import StringIO
+
+        o = StringIO()
+        padding = " " * len("array([")
+        for i, x in enumerate(self.xs):
+            if i == 0:
+                o.write("array([")
+            else:
+                o.write(",\n")
+                o.write(padding)
+            o.write(repr(x))
+        o.write("])")
+        return o.getvalue()
+
+
+def arange(*args):
+    return FakeArray(list(range(*args)))
+
+
+def _reshape(itr, units):
+    if not isinstance(units, (list, tuple)):  # integer?
+        return [next(itr) for _ in range(units)]
+    elif len(units) == 1:
+        return [next(itr) for _ in range(units[0])]
+    else:
+        return [_reshape(itr, units[1:]) for _ in range(units[0])]
diff --git a/pycomment/tests/test_capture.py b/pycomment/tests/test_capture.py
index 00e955b..5da6193 100644
--- a/pycomment/tests/test_capture.py
+++ b/pycomment/tests/test_capture.py
@@ -1,9 +1,10 @@
 import unittest
 import textwrap
 from collections import namedtuple
+from .testing import AssertDiffMixin
 
 
-class Tests(unittest.TestCase):
+class Tests(unittest.TestCase, AssertDiffMixin):
     def _callFUT(self, code):
         from pycomment.capture import capture
 
@@ -20,7 +21,7 @@ _ = ["hello", "bye"][0] + " world"  # => "hello world"
 print('ZZ\U000f0000ZZ1:', repr(_), 'ZZ\U000f0000ZZ', sep='')
                     """
                 ).strip(),
-                comments={"1": repr("hello world")},
+                comments={"1": [repr("hello world")]},
                 stdout=[],
             ),
             C(
@@ -49,7 +50,7 @@ print('ZZ\U000f0000ZZ6:', repr(_), 'ZZ\U000f0000ZZ', sep='')
 print("bye")
                     """
                 ).strip(),
-                comments={"5": "18", "6": "18"},
+                comments={"5": ["18"], "6": ["18"]},
                 stdout=["hello", "bye"],
             ),
             C(
@@ -77,7 +78,7 @@ print('ZZ\U000f0000ZZ10:', repr(_), 'ZZ\U000f0000ZZ', sep='')
 main()
                     """
                 ).strip(),
-                comments={"3": "60", "6": "25", "10": "6"},
+                comments={"3": ["60"], "6": ["25"], "10": ["6"]},
                 stdout=["Hoi"],
             ),
             C(
@@ -97,7 +98,26 @@ _ = x = 1 - 10  # => -9
 print('ZZ\U000f0000ZZ17:', repr(_), 'ZZ\U000f0000ZZ', sep='')
                     """
                 ).strip(),
-                comments={"2": "9", "4": "81", "6": "(1, 81, 9)", "17": "-9"},
+                comments={"2": ["9"], "4": ["81"], "6": ["(1, 81, 9)"], "17": ["-9"]},
+                stdout=[],
+            ),
+            C(
+                msg="_fakearray",
+                code=textwrap.dedent(
+                    """
+from pycomment.tests._fakearray import arange
+_  = arange(0, 9).reshape((3, 3))  # =>
+
+print('ZZ\U000f0000ZZ3:', repr(_), 'ZZ\U000f0000ZZ', sep='')
+                    """
+                ).strip(),
+                comments={
+                    "3": [
+                        "array([[0, 1, 2],",
+                        "       [3, 4, 5],",
+                        "       [6, 7, 8]])",
+                    ]
+                },
                 stdout=[],
             ),
         ]
@@ -105,4 +125,9 @@ print('ZZ\U000f0000ZZ17:', repr(_), 'ZZ\U000f0000ZZ', sep='')
             with self.subTest(msg=c.msg):
                 got = self._callFUT(c.code)
                 self.assertDictEqual(got.comments, c.comments)
-                self.assertEqual(got.stdout, c.stdout)
+                self.assertDiff(
+                    "\n".join(got.stdout),
+                    "\n".join(c.stdout),
+                    fromfile="got",
+                    tofile="want",
+                )
diff --git a/pycomment/tests/test_emit.py b/pycomment/tests/test_emit.py
index e20081e..503eca9 100644
--- a/pycomment/tests/test_emit.py
+++ b/pycomment/tests/test_emit.py
@@ -1,8 +1,12 @@
 import unittest
+import textwrap
 from collections import namedtuple
+from .testing import AssertDiffMixin
 
 
-class Tests(unittest.TestCase):
+class Tests(unittest.TestCase, AssertDiffMixin):
+    maxDiff = None
+
     def _callFUT(self, code: str, result_map, rest, out):
         from pycomment.emit import emit
         from io import StringIO
@@ -16,11 +20,78 @@ class Tests(unittest.TestCase):
         cases = [
             C(
                 msg="simplest",
-                code="""["hello", "bye"][0] + " world"  # => """,
-                comments={"1": repr("hello, world")},
+                code=textwrap.dedent(
+                    """
+                    ["hello", "bye"][0] + " world"  # => 
+                    """
+                ).strip(),
+                comments={"1": [repr("hello, world")]},
+                stdout=[],
+                want=textwrap.dedent(
+                    """
+                    ["hello", "bye"][0] + " world"  # => 'hello, world'
+                    """
+                ).strip(),
+            ),
+            # todo: stdout
+            C(
+                msg="multi-line",
+                code=textwrap.dedent(
+                    """
+                    from pycomment.tests._fakearray import arange
+                    arange(9).reshape((3, 3))  # =>
+                    """
+                ).strip(),
+                comments={
+                    "2": [
+                        "array([[0, 1, 2],",
+                        "       [3, 4, 5],",
+                        "       [6, 7, 8]])",
+                    ]
+                },
+                stdout=[],
+                want=textwrap.dedent(
+                    """
+                    from pycomment.tests._fakearray import arange
+                    arange(9).reshape((3, 3))  # => multi-line..
+                    # array([[0, 1, 2],
+                    #        [3, 4, 5],
+                    #        [6, 7, 8]])
+                    # ..multi-line
+                    """
+                ).strip(),
+            ),
+            C(
+                msg="multi-line replace",
+                code=textwrap.dedent(
+                    """
+                    from pycomment.tests._fakearray import arange
+                    arange(9).reshape((3, 3))  # => multi-line..
+                    # array([[0, 1, 2],
+                    #        [3, 4, 5],
+                    #        [6, 7, 8]])
+                    # ..multi-line
+                    """
+                ).strip(),
+                comments={
+                    "2": [
+                        "array([[0, 1, 2],",
+                        "       [3, 4, 5],",
+                        "       [6, 7, 8]])",
+                    ]
+                },
                 stdout=[],
-                want="""["hello", "bye"][0] + " world"  # => 'hello, world'""".strip(),
-            )
+                want=textwrap.dedent(
+                    """
+                    from pycomment.tests._fakearray import arange
+                    arange(9).reshape((3, 3))  # => multi-line..
+                    # array([[0, 1, 2],
+                    #        [3, 4, 5],
+                    #        [6, 7, 8]])
+                    # ..multi-line
+                    """
+                ).strip(),
+            ),
         ]
 
         for c in cases:
@@ -28,4 +99,4 @@ class Tests(unittest.TestCase):
                 o = StringIO()
                 self._callFUT(c.code, c.comments, c.stdout, out=o)
                 got = o.getvalue().strip()
-                self.assertEqual(got, c.want)
+                self.assertDiff(got, c.want, fromfile="got", tofile="want")
diff --git a/pycomment/tests/testing.py b/pycomment/tests/testing.py
new file mode 100644
index 0000000..29251b1
--- /dev/null
+++ b/pycomment/tests/testing.py
@@ -0,0 +1,31 @@
+import difflib
+
+# don't show this module's infor,  on traceback, when test is failed.
+__unittest = True
+
+
+class AssertDiffMixin:  # with unittest.TestCase
+    def assertDiff(self, first, second, *, msg=None, fromfile="first", tofile="second"):
+        self.assertIsInstance(first, str, "First argument is not a string")
+        self.assertIsInstance(second, str, "Second argument is not a string")
+
+        msg = msg or "{} != {}".format(repr(first)[:40], repr(second)[:40])
+
+        if first != second:
+            # don't use difflib if the strings are too long
+            if len(first) > self._diffThreshold or len(second) > self._diffThreshold:
+                self._baseAssertEqual(first, second, msg)
+
+            firstlines = first.splitlines(keepends=True)
+            secondlines = second.splitlines(keepends=True)
+            if not firstlines[-1].endswith("\n"):
+                firstlines[-1] = firstlines[-1] + "\n"
+            if not secondlines[-1].endswith("\n"):
+                secondlines[-1] = secondlines[-1] + "\n"
+
+            diff = "\n" + "".join(
+                difflib.unified_diff(
+                    firstlines, secondlines, fromfile=fromfile, tofile=tofile
+                )
+            )
+            raise self.fail(self._formatMessage(diff, msg))

pip install -e .[dev]
parse_log_pytest
requirements.txt
black pytest pytest-cov pytest-xdist pytest-mock pytest-asyncio
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
black==23.3.0
certifi @ file:///croot/certifi_1671487769961/work/certifi
click==8.1.8
coverage==7.2.7
exceptiongroup==1.2.2
execnet==2.0.2
importlib-metadata==6.7.0
iniconfig==2.0.0
mypy-extensions==1.0.0
packaging==24.0
pathspec==0.11.2
platformdirs==4.0.0
pluggy==1.2.0
-e git+https://github.com/podhmo/pycomment.git@696b8d7e7e4084c8ea3174dac7e8158fac4dd71e#egg=pycomment
pytest==7.4.4
pytest-asyncio==0.21.2
pytest-cov==4.1.0
pytest-mock==3.11.1
pytest-xdist==3.5.0
tomli==2.0.1
typed-ast==1.5.5
typing_extensions==4.7.1
zipp==3.15.0

name: pycomment
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - black==23.3.0
      - click==8.1.8
      - coverage==7.2.7
      - exceptiongroup==1.2.2
      - execnet==2.0.2
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - mypy-extensions==1.0.0
      - packaging==24.0
      - pathspec==0.11.2
      - platformdirs==4.0.0
      - pluggy==1.2.0
      - pytest==7.4.4
      - pytest-asyncio==0.21.2
      - pytest-cov==4.1.0
      - pytest-mock==3.11.1
      - pytest-xdist==3.5.0
      - tomli==2.0.1
      - typed-ast==1.5.5
      - typing-extensions==4.7.1
      - zipp==3.15.0
prefix: /opt/conda/envs/pycomment

pycomment/tests/test_capture.py::Tests::test_it pycomment/tests/test_emit.py::Tests::test_it
README.rst pycomment/emit.py examples/03loop.py examples/04multi-line.py pycomment/langhelpers.py pycomment/capture.py
README.rst pycomment/emit.py examples/03loop.py examples/04multi-line.py pycomment/langhelpers.py pycomment/capture.py