Repository: pandas-dev/pandas

Issue:
Misleading read_csv() error message. It says lines but refers to rows.
This is not an error per se but might misdirect others as it did to me.

```python
from io import StringIO
# if not using python 3:
# from StringIO import StringIO 
import pandas as pd

TESTDATA = StringIO("""c1;c2;c3
    1,"some text"
    2,"some more 
    text that is 
    multiple lines"
    3,"more text"
    4,"text with missing quote
    """)

pd.read_csv(TESTDATA)

```
#### Problem description

This code results in the following message

`ParserError: Error tokenizing data. C error: EOF inside string starting at line 4`
which seems correct at first sight. But actually the problematic string does not start at line 4 it starts at **row 4**. I  was working with a very large file and did not have the chance to debug it easily. I spendt considerable time trying to figure out the problem with that line in the file.

In a csv file every line does not necessarily correspond to a row.

#### Expected Output
`ParserError: Error tokenizing data. C error: EOF inside string starting at row 4`

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.6.5.final.0
python-bits: 64
OS: Linux
OS-release: 4.16.7-041607-generic
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8
LOCALE: en_US.UTF-8

pandas: 0.22.0
pytest: 3.5.1
pip: 18.0
setuptools: 39.1.0
Cython: 0.28.2
numpy: 1.14.3
scipy: 1.1.0
pyarrow: None
xarray: None
IPython: 6.4.0
sphinx: 1.7.4
patsy: 0.5.0
dateutil: 2.7.3
pytz: 2018.4
blosc: None
bottleneck: 1.2.1
tables: 3.4.3
numexpr: 2.6.5
feather: None
matplotlib: 2.2.2
openpyxl: 2.5.3
xlrd: 1.1.0
xlwt: 1.3.0
xlsxwriter: 1.0.4
lxml: 4.2.1
bs4: 4.6.0
html5lib: 1.0.1
sqlalchemy: 1.2.7
pymysql: None
psycopg2: None
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None

</details>



Discussion:
I'm not clear on the distinction you are trying to make - can you provide a more illustrative example?
@WillAyd : `TESTDATA` has 8 lines (`len(TESTDATA.readlines())`), which, if correctly parsed, should result in a `DataFrame` with 4 rows. In line 7 (1-based numbering)
```
    4,"text with missing quote
```
 there is an error, which prevents the correct parsing of row 4 (1-based numbering not counting the header row, or 0-based counting also the header row).

I guess that the error message (sorry I had no time to check the source) refers to a "logical" line 4 (0-based), where multi-line strings have already collapsed into a single logical line.

IMHO error messages should contain an unambiguous reference to the line/column (1-based) of the original input file.
This proposal seems fine to me.

@miccoli : Would you like to submit a PR for this?
@gfyoung  : I know, when you suggest an improvement you are morally obliged to implement it! 😃 Unfortunately I have to decline, since I have no spare time at this moment to dedicate for a new PR.
@miccoli : No worries!  Time is a limited resource, no need to remind me 😉 

Resolving patch:
diff --git a/pandas/io/parsers.py b/pandas/io/parsers.py
--- a/pandas/io/parsers.py
+++ b/pandas/io/parsers.py
@@ -2727,9 +2727,6 @@ def _next_iter_line(self, row_num):
                            'cannot be processed in Python\'s '
                            'native csv library at the moment, '
                            'so please pass in engine=\'c\' instead')
-                elif 'newline inside string' in msg:
-                    msg = ('EOF inside string starting with '
-                           'line ' + str(row_num))
 
                 if self.skipfooter > 0:
                     reason = ('Error could possibly be due to '


Test patch:
diff --git a/pandas/tests/io/parser/common.py b/pandas/tests/io/parser/common.py
--- a/pandas/tests/io/parser/common.py
+++ b/pandas/tests/io/parser/common.py
@@ -197,20 +197,6 @@ def test_malformed(self):
                                 header=1, comment='#',
                                 skipfooter=1)
 
-    def test_quoting(self):
-        bad_line_small = """printer\tresult\tvariant_name
-Klosterdruckerei\tKlosterdruckerei <Salem> (1611-1804)\tMuller, Jacob
-Klosterdruckerei\tKlosterdruckerei <Salem> (1611-1804)\tMuller, Jakob
-Klosterdruckerei\tKlosterdruckerei <Kempten> (1609-1805)\t"Furststiftische Hofdruckerei,  <Kempten""
-Klosterdruckerei\tKlosterdruckerei <Kempten> (1609-1805)\tGaller, Alois
-Klosterdruckerei\tKlosterdruckerei <Kempten> (1609-1805)\tHochfurstliche Buchhandlung <Kempten>"""  # noqa
-        pytest.raises(Exception, self.read_table, StringIO(bad_line_small),
-                      sep='\t')
-
-        good_line_small = bad_line_small + '"'
-        df = self.read_table(StringIO(good_line_small), sep='\t')
-        assert len(df) == 3
-
     def test_unnamed_columns(self):
         data = """A,B,C,,
 1,2,3,4,5
diff --git a/pandas/tests/io/parser/quoting.py b/pandas/tests/io/parser/quoting.py
--- a/pandas/tests/io/parser/quoting.py
+++ b/pandas/tests/io/parser/quoting.py
@@ -9,6 +9,7 @@
 import pandas.util.testing as tm
 
 from pandas import DataFrame
+from pandas.errors import ParserError
 from pandas.compat import PY3, StringIO, u
 
 
@@ -151,3 +152,19 @@ def test_quotechar_unicode(self):
         if PY3:
             result = self.read_csv(StringIO(data), quotechar=u('\u0001'))
             tm.assert_frame_equal(result, expected)
+
+    def test_unbalanced_quoting(self):
+        # see gh-22789.
+        data = "a,b,c\n1,2,\"3"
+
+        if self.engine == "c":
+            regex = "EOF inside string starting at row 1"
+        else:
+            regex = "unexpected end of data"
+
+        with tm.assert_raises_regex(ParserError, regex):
+            self.read_csv(StringIO(data))
+
+        expected = DataFrame([[1, 2, 3]], columns=["a", "b", "c"])
+        data = self.read_csv(StringIO(data + '"'))
+        tm.assert_frame_equal(data, expected)