Skip to content

Commit c528f7c

Browse files
gh-155097: Add converter and formatter parameters in csv.reader and csv.writer
They are used instead of float() and str() for converting between fields and values. Both take the 0-based position of the field in the row as the first argument, so the conversion can depend on the column. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 7d76013 commit c528f7c

5 files changed

Lines changed: 203 additions & 15 deletions

File tree

Doc/library/csv.rst

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ The :mod:`!csv` module defines the following functions:
5151
.. index::
5252
single: universal newlines; csv.reader function
5353

54-
.. function:: reader(csvfile, /, dialect='excel', **fmtparams)
54+
.. function:: reader(csvfile, /, dialect='excel', *, converter=None, **fmtparams)
5555

5656
Return a :ref:`reader object <reader-objects>` that will process
5757
lines from the given *csvfile*. A csvfile must be an iterable of
@@ -69,7 +69,11 @@ The :mod:`!csv` module defines the following functions:
6969

7070
Each row read from the csv file is returned as a list of strings. No
7171
automatic data type conversion is performed unless the :data:`QUOTE_NONNUMERIC` format
72-
option is specified (in which case unquoted fields are transformed into floats).
72+
option is specified,
73+
in which case unquoted fields are transformed with the optional *converter* argument,
74+
or into floats if it is not given.
75+
*converter* is called as ``converter(index, field)``,
76+
where *index* is the 0-based position of the field in the row.
7377

7478
A short usage example::
7579

@@ -88,8 +92,11 @@ The :mod:`!csv` module defines the following functions:
8892
Spam Spam Spam Spam Spam |Baked Beans|
8993
Spam |Lovely Spam| |Wonderful Spam|
9094
95+
.. versionadded:: next
96+
The *converter* parameter.
9197

92-
.. function:: writer(csvfile, /, dialect='excel', **fmtparams)
98+
99+
.. function:: writer(csvfile, /, dialect='excel', *, formatter=None, **fmtparams)
93100

94101
Return a writer object responsible for converting the user's data into delimited
95102
strings on the given file-like object. *csvfile* can be any object with a
@@ -101,12 +108,20 @@ The :mod:`!csv` module defines the following functions:
101108
:func:`list_dialects` function. The other optional *fmtparams* keyword arguments
102109
can be given to override individual formatting parameters in the current
103110
dialect. For full details about dialects and formatting parameters, see
104-
the :ref:`csv-fmt-params` section. To make it
105-
as easy as possible to interface with modules which implement the DB API, the
106-
value :const:`None` is written as the empty string. While this isn't a
107-
reversible transformation, it makes it easier to dump SQL NULL data values to
108-
CSV files without preprocessing the data returned from a ``cursor.fetch*`` call.
109-
All other non-string data are stringified with :func:`str` before being written.
111+
the :ref:`csv-fmt-params` section.
112+
113+
To make it as easy as possible to interface with modules which implement the DB API,
114+
the value :const:`None` is written as the empty string.
115+
While this isn't a reversible transformation,
116+
it makes it easier to dump SQL NULL data values to CSV files
117+
without preprocessing the data returned from a ``cursor.fetch*`` call.
118+
All other non-string data are stringified before being written
119+
with the optional *formatter* argument,
120+
or with :func:`str` if it is not given.
121+
*formatter* is called as ``formatter(index, value)``,
122+
where *index* is the 0-based position of the field in the row,
123+
and must return a string.
124+
Quoting is still determined by the original value.
110125

111126
A short usage example::
112127

@@ -124,6 +139,9 @@ The :mod:`!csv` module defines the following functions:
124139
Spam Spam Spam Spam Spam |Baked Beans|
125140
Spam |Lovely Spam| |Wonderful Spam|
126141
142+
.. versionadded:: next
143+
The *formatter* parameter.
144+
127145

128146
.. function:: register_dialect(name, /, dialect='excel', **fmtparams)
129147

Doc/whatsnew/3.16.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@ csv
120120
The results may differ from those of earlier Python versions.
121121
(Contributed by Serhiy Storchaka in :gh:`83273`.)
122122

123+
* Add the *converter* parameter in :func:`csv.reader`
124+
and the *formatter* parameter in :func:`csv.writer`.
125+
They are used instead of :func:`float` and :func:`str`
126+
for converting between fields and values,
127+
and allow to convert and format the fields depending on the column.
128+
(Contributed by Serhiy Storchaka in :gh:`155097`.)
129+
123130
curses
124131
------
125132

Lib/test/test_csv.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,88 @@ def test_read_quoting(self):
460460
self._read_test(['1\\.5,\\.5,"\\.5"'], [[1.5, 0.5, ".5"]],
461461
quoting=csv.QUOTE_STRINGS, escapechar='\\')
462462

463+
def test_read_converter(self):
464+
def converter(index, field):
465+
calls.append((index, field))
466+
return types[index](field)
467+
468+
types = [str, int, complex]
469+
calls = []
470+
self._read_test(['spam,42,1j'], [['spam', 42, 1j]],
471+
quoting=csv.QUOTE_NONNUMERIC, converter=converter)
472+
self.assertEqual(calls, [(0, 'spam'), (1, '42'), (2, '1j')])
473+
474+
# The index is the position in the record and is reset for each record.
475+
types = [int] * 3
476+
calls = []
477+
self._read_test(['1,2,3', '4,5', '6'], [[1, 2, 3], [4, 5], [6]],
478+
quoting=csv.QUOTE_STRINGS, converter=converter)
479+
self.assertEqual([index for index, field in calls],
480+
[0, 1, 2, 0, 1, 0])
481+
482+
# Quoted and empty fields are not converted.
483+
types = [str] * 3
484+
calls = []
485+
self._read_test(['"spam",,42'], [['spam', '', '42']],
486+
quoting=csv.QUOTE_NONNUMERIC, converter=converter)
487+
self.assertEqual(calls, [(2, '42')])
488+
489+
# Other quoting modes do not convert at all.
490+
self._read_test(['1,2'], [['1', '2']],
491+
converter=lambda index, field: int(field))
492+
self._read_test(['1,2'], [['1', '2']],
493+
quoting=csv.QUOTE_ALL,
494+
converter=lambda index, field: int(field))
495+
496+
# None means the default conversion.
497+
self._read_test(['1,2'], [[1.0, 2.0]],
498+
quoting=csv.QUOTE_NONNUMERIC, converter=None)
499+
500+
def test_read_converter_errors(self):
501+
with self.assertRaisesRegex(TypeError, 'must be callable or None'):
502+
csv.reader([], converter='int')
503+
with self.assertRaises(ZeroDivisionError):
504+
self._read_test(['1,2'], [], quoting=csv.QUOTE_NONNUMERIC,
505+
converter=lambda index, field: 1/0)
506+
# A one-argument callable does not fit.
507+
with self.assertRaises(TypeError):
508+
self._read_test(['1,2'], [], quoting=csv.QUOTE_NONNUMERIC,
509+
converter=float)
510+
511+
def test_write_formatter(self):
512+
def formatter(index, value):
513+
calls.append((index, value))
514+
return format(value, '.2f') if index == 2 else str(value)
515+
516+
calls = []
517+
self._write_test(['a', 1, 0.0, 3.14159], 'a,1,0.00,3.14159',
518+
formatter=formatter)
519+
self.assertEqual(calls, [(1, 1), (2, 0.0), (3, 3.14159)])
520+
521+
# Strings and None are not passed to the formatter.
522+
calls = []
523+
self._write_test([0, 'a', None, 3], '<0>,a,,<3>',
524+
formatter=lambda index, value:
525+
calls.append(value) or f'<{index}>')
526+
self.assertEqual(calls, [0, 3])
527+
528+
# Quoting is decided by the original value, not by the result.
529+
self._write_test([1.5, 'a'], '1.50,"a"', quoting=csv.QUOTE_NONNUMERIC,
530+
formatter=lambda index, value: format(value, '.2f'))
531+
532+
# None means str().
533+
self._write_test([1, 2], '1,2', formatter=None)
534+
535+
def test_write_formatter_errors(self):
536+
with self.assertRaisesRegex(TypeError, 'must be callable or None'):
537+
csv.writer(StringIO(), formatter='str')
538+
with self.assertRaisesRegex(csv.Error, 'must return a string'):
539+
self._write_test([1], '', formatter=lambda index, value: index)
540+
self._write_error_test(ZeroDivisionError, [1],
541+
formatter=lambda index, value: 1/0)
542+
# A one-argument callable does not fit.
543+
self._write_error_test(TypeError, [1], formatter=repr)
544+
463545
def test_read_skipinitialspace(self):
464546
self._read_test(['no space, space, spaces,\ttab'],
465547
[['no space', 'space', 'spaces', '\ttab']],
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add the *converter* parameter in :func:`csv.reader` and the *formatter*
2+
parameter in :func:`csv.writer`. They are used instead of :func:`float` and
3+
:func:`str` for converting between fields and values.

Modules/_csv.c

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ typedef struct {
127127

128128
DialectObj *dialect; /* parsing dialect */
129129

130+
PyObject *converter; /* called to convert an unquoted field, or NULL */
131+
130132
PyObject *fields; /* field list for current record */
131133
ParserState state; /* current CSV parse state */
132134
Py_UCS4 *field; /* temporary buffer */
@@ -143,6 +145,8 @@ typedef struct {
143145

144146
DialectObj *dialect; /* parsing dialect */
145147

148+
PyObject *formatter; /* called to convert a value to a string, or NULL */
149+
146150
Py_UCS4 *rec; /* buffer for parser.join */
147151
Py_ssize_t rec_size; /* size of allocated record */
148152
Py_ssize_t rec_len; /* length of record */
@@ -651,6 +655,40 @@ _call_dialect(_csvstate *module_state, PyObject *dialect_inst, PyObject *kwargs)
651655
}
652656
}
653657

658+
/* Pop the callable *name* out of *kwargs, replacing it with a copy.
659+
*result is set to NULL if it is not given or is None. */
660+
static int
661+
pop_callable_kwarg(const char *name, PyObject **kwargs, PyObject **result)
662+
{
663+
PyObject *value;
664+
*result = NULL;
665+
if (*kwargs == NULL) {
666+
return 0;
667+
}
668+
int rc = PyDict_GetItemStringRef(*kwargs, name, &value);
669+
if (rc <= 0) { /* not found or error */
670+
return rc;
671+
}
672+
if (value == Py_None) {
673+
Py_CLEAR(value);
674+
}
675+
else if (!PyCallable_Check(value)) {
676+
PyErr_Format(PyExc_TypeError,
677+
"\"%s\" must be callable or None, not %T", name, value);
678+
Py_DECREF(value);
679+
return -1;
680+
}
681+
PyObject *copy = PyDict_Copy(*kwargs);
682+
if (copy == NULL || PyDict_DelItemString(copy, name) < 0) {
683+
Py_XDECREF(copy);
684+
Py_XDECREF(value);
685+
return -1;
686+
}
687+
*kwargs = copy;
688+
*result = value;
689+
return 0;
690+
}
691+
654692
/*
655693
* READER
656694
*/
@@ -676,7 +714,14 @@ parse_save_field(ReaderObj *self)
676714
self->field_len != 0 &&
677715
(quoting == QUOTE_NONNUMERIC || quoting == QUOTE_STRINGS))
678716
{
679-
PyObject *tmp = PyNumber_Float(field);
717+
PyObject *tmp;
718+
if (self->converter != NULL) {
719+
tmp = PyObject_CallFunction(self->converter, "nO",
720+
PyList_GET_SIZE(self->fields), field);
721+
}
722+
else {
723+
tmp = PyNumber_Float(field);
724+
}
680725
Py_DECREF(field);
681726
if (tmp == NULL) {
682727
return -1;
@@ -1025,6 +1070,7 @@ Reader_traverse(PyObject *op, visitproc visit, void *arg)
10251070
{
10261071
ReaderObj *self = _ReaderObj_CAST(op);
10271072
Py_VISIT(self->dialect);
1073+
Py_VISIT(self->converter);
10281074
Py_VISIT(self->input_iter);
10291075
Py_VISIT(self->fields);
10301076
Py_VISIT(Py_TYPE(self));
@@ -1036,6 +1082,7 @@ Reader_clear(PyObject *op)
10361082
{
10371083
ReaderObj *self = _ReaderObj_CAST(op);
10381084
Py_CLEAR(self->dialect);
1085+
Py_CLEAR(self->converter);
10391086
Py_CLEAR(self->input_iter);
10401087
Py_CLEAR(self->fields);
10411088
return 0;
@@ -1096,6 +1143,7 @@ csv_reader(PyObject *module, PyObject *args, PyObject *keyword_args)
10961143
return NULL;
10971144

10981145
self->dialect = NULL;
1146+
self->converter = NULL;
10991147
self->fields = NULL;
11001148
self->input_iter = NULL;
11011149
self->field = NULL;
@@ -1116,8 +1164,15 @@ csv_reader(PyObject *module, PyObject *args, PyObject *keyword_args)
11161164
Py_DECREF(self);
11171165
return NULL;
11181166
}
1119-
self->dialect = (DialectObj *)_call_dialect(module_state, dialect,
1120-
keyword_args);
1167+
PyObject *kwargs = keyword_args;
1168+
if (pop_callable_kwarg("converter", &kwargs, &self->converter) < 0) {
1169+
Py_DECREF(self);
1170+
return NULL;
1171+
}
1172+
self->dialect = (DialectObj *)_call_dialect(module_state, dialect, kwargs);
1173+
if (kwargs != keyword_args) {
1174+
Py_DECREF(kwargs);
1175+
}
11211176
if (self->dialect == NULL) {
11221177
Py_DECREF(self);
11231178
return NULL;
@@ -1344,6 +1399,7 @@ csv_writerow_lock_held(PyObject *op, PyObject *seq)
13441399
/* Join all fields in internal buffer.
13451400
*/
13461401
join_reset(self);
1402+
Py_ssize_t field_index = 0;
13471403
while ((field = PyIter_Next(iter))) {
13481404
int append_ok;
13491405
int quoted;
@@ -1378,7 +1434,18 @@ csv_writerow_lock_held(PyObject *op, PyObject *seq)
13781434
else {
13791435
PyObject *str;
13801436

1381-
str = PyObject_Str(field);
1437+
if (self->formatter != NULL) {
1438+
str = PyObject_CallFunction(self->formatter, "nO",
1439+
field_index, field);
1440+
if (str != NULL && !PyUnicode_Check(str)) {
1441+
PyErr_Format(self->error_obj,
1442+
"formatter must return a string, not %T", str);
1443+
Py_CLEAR(str);
1444+
}
1445+
}
1446+
else {
1447+
str = PyObject_Str(field);
1448+
}
13821449
Py_DECREF(field);
13831450
if (str == NULL) {
13841451
Py_DECREF(iter);
@@ -1391,6 +1458,7 @@ csv_writerow_lock_held(PyObject *op, PyObject *seq)
13911458
Py_DECREF(iter);
13921459
return NULL;
13931460
}
1461+
field_index++;
13941462
}
13951463
Py_DECREF(iter);
13961464
if (PyErr_Occurred())
@@ -1496,6 +1564,7 @@ Writer_traverse(PyObject *op, visitproc visit, void *arg)
14961564
{
14971565
WriterObj *self = _WriterObj_CAST(op);
14981566
Py_VISIT(self->dialect);
1567+
Py_VISIT(self->formatter);
14991568
Py_VISIT(self->write);
15001569
Py_VISIT(self->error_obj);
15011570
Py_VISIT(Py_TYPE(self));
@@ -1507,6 +1576,7 @@ Writer_clear(PyObject *op)
15071576
{
15081577
WriterObj *self = _WriterObj_CAST(op);
15091578
Py_CLEAR(self->dialect);
1579+
Py_CLEAR(self->formatter);
15101580
Py_CLEAR(self->write);
15111581
Py_CLEAR(self->error_obj);
15121582
return 0;
@@ -1564,6 +1634,7 @@ csv_writer(PyObject *module, PyObject *args, PyObject *keyword_args)
15641634

15651635
self->dialect = NULL;
15661636
self->write = NULL;
1637+
self->formatter = NULL;
15671638

15681639
self->rec = NULL;
15691640
self->rec_size = 0;
@@ -1588,8 +1659,15 @@ csv_writer(PyObject *module, PyObject *args, PyObject *keyword_args)
15881659
Py_DECREF(self);
15891660
return NULL;
15901661
}
1591-
self->dialect = (DialectObj *)_call_dialect(module_state, dialect,
1592-
keyword_args);
1662+
PyObject *kwargs = keyword_args;
1663+
if (pop_callable_kwarg("formatter", &kwargs, &self->formatter) < 0) {
1664+
Py_DECREF(self);
1665+
return NULL;
1666+
}
1667+
self->dialect = (DialectObj *)_call_dialect(module_state, dialect, kwargs);
1668+
if (kwargs != keyword_args) {
1669+
Py_DECREF(kwargs);
1670+
}
15931671
if (self->dialect == NULL) {
15941672
Py_DECREF(self);
15951673
return NULL;

0 commit comments

Comments
 (0)