hynek: Hm yeah this is a regression. It should be pulled out and protected using an `if not isinstance(event, (six.text_type, bytes))` I think.
hynek: Ping?
therefromhere: Sorry, I haven't had a chance to work on this.
therefromhere: I'd be happy to close this for now and reopen once py2 support has been dropped?
hynek: Python 2 hasn’t been dropped for structlog and I don’t plan on it anytime soon because I need it myself in the foreseeable future. 😇   We’ll only be removing 3.4 from CI in the next release.
magicalbanana: @hynek will this get merged anytime soon? I'm finding myself the need to log in a dict but be renderable as as string in development for pretty output.
therefromhere: @magicalbanana not speaking for @hynek , but unlikely, since it doesn't support py2.  I'd be happy to close since I probably won't have time to make it robustly py2 compatible soon, unless someone else wants to pick this up?
hynek: I think this might be much easier than thought since structlog is depending on `six`. I suspect it might be enough to replace `str` by `six.text_type`?
therefromhere: > I think this might be much easier than thought since structlog is depending on `six`. I suspect it might be enough to replace `str` by `six.text_type`?

Possibly?  Though I think that would subtly change the behavior on py2, because that would force event etc to be unicode strings, whereas currently they'll be byte strings (assuming that's what `event_dict["event"]` is).
hynek: I keep forgetting how to push to other people's branches, so here's my suggested solution:

```patch
diff --git src/structlog/dev.py src/structlog/dev.py
index 42bb10a..372f7fc 100644
--- src/structlog/dev.py
+++ src/structlog/dev.py
@@ -8,7 +8,7 @@ Helpers that make development with ``structlog`` more pleasant.
 
 from __future__ import absolute_import, division, print_function
 
-from six import StringIO
+from six import PY2, StringIO, string_types
 
 
 try:
@@ -214,7 +214,10 @@ class ConsoleRenderer(object):
             )
 
         # force event to str for compatibility with standard library
-        event = str(event_dict.pop("event"))
+        event = event_dict.pop("event")
+        if not PY2 or not isinstance(event, string_types):
+            event = str(event)
+
         if event_dict:
             event = _pad(event, self._pad_event) + self._styles.reset + " "
         else:
diff --git tests/test_dev.py tests/test_dev.py
index 8da5a15..5e0b0c2 100644
--- tests/test_dev.py
+++ tests/test_dev.py
@@ -91,6 +91,17 @@ class TestConsoleRenderer(object):
 
         assert unpadded == rv
 
+    @pytest.mark.skipif(not six.PY2, reason="Problem only exists on Python 2.")
+    @pytest.mark.parametrize("s", [u"\xc3\xa4".encode("utf-8"), u"ä", "ä"])
+    def test_event_py2_only_stringify_non_strings(self, cr, s, styles):
+        """
+        If event is a string type already, leave it be on Python 2. Running
+        str() on unicode strings with non-ascii characters raises an error.
+        """
+        rv = cr(None, None, {"event": s})
+
+        assert styles.bright + s + styles.reset == rv
+
     def test_level(self, cr, styles, padded):
         """
         Levels are rendered aligned, in square brackets, and color coded.

```

Does that make sense?

I'm honestly a bit surprised that StringIO makes this work but I'll take it I guess…
therefromhere: Thanks, yeah that seems reasonable.  I've added your patch as is.
diff --git a/src/structlog/dev.py b/src/structlog/dev.py
index 0160650..372f7fc 100644
--- a/src/structlog/dev.py
+++ b/src/structlog/dev.py
@@ -8,7 +8,7 @@ Helpers that make development with ``structlog`` more pleasant.
 
 from __future__ import absolute_import, division, print_function
 
-from six import StringIO
+from six import PY2, StringIO, string_types
 
 
 try:
@@ -213,7 +213,11 @@ class ConsoleRenderer(object):
                 + "] "
             )
 
+        # force event to str for compatibility with standard library
         event = event_dict.pop("event")
+        if not PY2 or not isinstance(event, string_types):
+            event = str(event)
+
         if event_dict:
             event = _pad(event, self._pad_event) + self._styles.reset + " "
         else:

ConsoleRender accepts only string as input
Hello :)

First of all I would like to thank you for this very handy package. 

I have observed the following:

```python
import structlog

structlog.configure(
    processors=[
        structlog.stdlib.add_logger_name,
        structlog.stdlib.add_log_level,
        structlog.stdlib.PositionalArgumentsFormatter(),
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.StackInfoRenderer(),
        structlog.processors.format_exc_info,
        structlog.dev.ConsoleRenderer()
    ],
    context_class=dict,
    logger_factory=structlog.stdlib.LoggerFactory(),
    wrapper_class=structlog.stdlib.BoundLogger,
    cache_logger_on_first_use=True,
)

mylogger = structlog.get_logger('foo').new()

mylogger.info('bar')
>> 2018-11-08T10:49:46.903028Z [info  ] bar              [foo]

mylogger.info({'value': 'bar'})
>> Raises TypeError: unsupported operand type(s) for +: 'dict' and 'str'
```

After checking the code for ConsoleRender, it seems that it has been implemented to accept only str as value to log. On the other hand when using JSONRenderer, it logs without any problem a single dictionary as log message. Is this by design ? Am I missing something ?
diff --git a/tests/test_dev.py b/tests/test_dev.py
index ee048a4..11690ba 100644
--- a/tests/test_dev.py
+++ b/tests/test_dev.py
@@ -81,6 +81,27 @@ class TestConsoleRenderer(object):
 
         assert (styles.timestamp + "42" + styles.reset + " " + unpadded) == rv
 
+    def test_event_stringified(self, cr, styles, unpadded):
+        """
+        Event is cast to string.
+        """
+        not_a_string = Exception("test")
+
+        rv = cr(None, None, {"event": not_a_string})
+
+        assert unpadded == rv
+
+    @pytest.mark.skipif(not six.PY2, reason="Problem only exists on Python 2.")
+    @pytest.mark.parametrize("s", [u"\xc3\xa4".encode("utf-8"), u"ä", "ä"])
+    def test_event_py2_only_stringify_non_strings(self, cr, s, styles):
+        """
+        If event is a string type already, leave it be on Python 2. Running
+        str() on unicode strings with non-ascii characters raises an error.
+        """
+        rv = cr(None, None, {"event": s})
+
+        assert styles.bright + s + styles.reset == rv
+
     def test_level(self, cr, styles, padded):
         """
         Levels are rendered aligned, in square brackets, and color coded.

pip install -e .[dev]
parse_log_pytest
requirements.txt
3.9
python -m pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.16
attrs==25.3.0
Automat==24.8.1
babel==2.17.0
certifi==2025.1.31
cfgv==3.4.0
charset-normalizer==3.4.1
constantly==23.10.4
coverage==7.8.0
distlib==0.3.9
docutils==0.21.2
exceptiongroup==1.2.2
filelock==3.18.0
freezegun==1.5.1
hyperlink==21.0.0
identify==2.6.9
idna==3.10
imagesize==1.4.1
importlib_metadata==8.6.1
incremental==24.7.2
iniconfig==2.1.0
Jinja2==3.1.6
MarkupSafe==3.0.2
nodeenv==1.9.1
packaging==24.2
platformdirs==4.3.7
pluggy==1.5.0
pre_commit==4.2.0
pretend==1.0.9
Pygments==2.19.1
pytest==8.3.5
python-dateutil==2.9.0.post0
python-rapidjson==1.20
PyYAML==6.0.2
requests==2.32.3
simplejson==3.20.1
six==1.17.0
snowballstemmer==2.2.0
Sphinx==7.4.7
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
-e git+https://github.com/hynek/structlog.git@8d3f31517caf2b877fb0dddaa61a81492d20c325#egg=structlog
tomli==2.2.1
Twisted==24.11.0
typing_extensions==4.13.0
urllib3==2.3.0
virtualenv==20.29.3
zipp==3.21.0
zope.interface==7.2

name: structlog
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
  - 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=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.16
      - attrs==25.3.0
      - automat==24.8.1
      - babel==2.17.0
      - certifi==2025.1.31
      - cfgv==3.4.0
      - charset-normalizer==3.4.1
      - constantly==23.10.4
      - coverage==7.8.0
      - distlib==0.3.9
      - docutils==0.21.2
      - exceptiongroup==1.2.2
      - filelock==3.18.0
      - freezegun==1.5.1
      - hyperlink==21.0.0
      - identify==2.6.9
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==8.6.1
      - incremental==24.7.2
      - iniconfig==2.1.0
      - jinja2==3.1.6
      - markupsafe==3.0.2
      - nodeenv==1.9.1
      - packaging==24.2
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pre-commit==4.2.0
      - pretend==1.0.9
      - pygments==2.19.1
      - pytest==8.3.5
      - python-dateutil==2.9.0.post0
      - python-rapidjson==1.20
      - pyyaml==6.0.2
      - requests==2.32.3
      - simplejson==3.20.1
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==7.4.7
      - sphinxcontrib-applehelp==2.0.0
      - sphinxcontrib-devhelp==2.0.0
      - sphinxcontrib-htmlhelp==2.1.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==2.0.0
      - sphinxcontrib-serializinghtml==2.0.0
      - structlog==19.2.0.dev0
      - tomli==2.2.1
      - twisted==24.11.0
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - virtualenv==20.29.3
      - zipp==3.21.0
      - zope-interface==7.2
prefix: /opt/conda/envs/structlog

tests/test_dev.py::TestConsoleRenderer::test_event_stringified
tests/test_dev.py::TestPad::test_normal tests/test_dev.py::TestPad::test_negative tests/test_dev.py::TestConsoleRenderer::test_missing_colorama tests/test_dev.py::TestConsoleRenderer::test_plain tests/test_dev.py::TestConsoleRenderer::test_timestamp tests/test_dev.py::TestConsoleRenderer::test_level tests/test_dev.py::TestConsoleRenderer::test_init_accepts_overriding_levels tests/test_dev.py::TestConsoleRenderer::test_logger_name tests/test_dev.py::TestConsoleRenderer::test_key_values tests/test_dev.py::TestConsoleRenderer::test_exception tests/test_dev.py::TestConsoleRenderer::test_stack_info tests/test_dev.py::TestConsoleRenderer::test_pad_event_param tests/test_dev.py::TestConsoleRenderer::test_everything tests/test_dev.py::TestConsoleRenderer::test_colorama_colors_false tests/test_dev.py::TestConsoleRenderer::test_colorama_force_colors tests/test_dev.py::TestConsoleRenderer::test_repr_native_str[True] tests/test_dev.py::TestConsoleRenderer::test_repr_native_str[False] tests/test_dev.py::TestConsoleRenderer::test_pickle[0-True-True] tests/test_dev.py::TestConsoleRenderer::test_pickle[0-True-False] tests/test_dev.py::TestConsoleRenderer::test_pickle[0-False-True] tests/test_dev.py::TestConsoleRenderer::test_pickle[0-False-False] tests/test_dev.py::TestConsoleRenderer::test_pickle[1-True-True] tests/test_dev.py::TestConsoleRenderer::test_pickle[1-True-False] tests/test_dev.py::TestConsoleRenderer::test_pickle[1-False-True] tests/test_dev.py::TestConsoleRenderer::test_pickle[1-False-False] tests/test_dev.py::TestConsoleRenderer::test_pickle[2-True-True] tests/test_dev.py::TestConsoleRenderer::test_pickle[2-True-False] tests/test_dev.py::TestConsoleRenderer::test_pickle[2-False-True] tests/test_dev.py::TestConsoleRenderer::test_pickle[2-False-False] tests/test_dev.py::TestConsoleRenderer::test_pickle[3-True-True] tests/test_dev.py::TestConsoleRenderer::test_pickle[3-True-False] tests/test_dev.py::TestConsoleRenderer::test_pickle[3-False-True] tests/test_dev.py::TestConsoleRenderer::test_pickle[3-False-False] tests/test_dev.py::TestConsoleRenderer::test_pickle[4-True-True] tests/test_dev.py::TestConsoleRenderer::test_pickle[4-True-False] tests/test_dev.py::TestConsoleRenderer::test_pickle[4-False-True] tests/test_dev.py::TestConsoleRenderer::test_pickle[4-False-False] tests/test_dev.py::TestSetExcInfo::test_wrong_name tests/test_dev.py::TestSetExcInfo::test_already_set[False] tests/test_dev.py::TestSetExcInfo::test_already_set[None] tests/test_dev.py::TestSetExcInfo::test_already_set[ei2] tests/test_dev.py::TestSetExcInfo::test_set_it
src/structlog/dev.py
src/structlog/dev.py