diff --git a/.travis.yml b/.travis.yml
index 4b1f886..b26433a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,6 @@
 language: python
 cache: pip
 os: linux
-dist: xenial
 
 jobs:
   include:
@@ -21,10 +20,8 @@ jobs:
       env: TOXENV=py38
     - python: "pypy"
       env: TOXENV=pypy
-      dist: trusty
     - python: "pypy3"
       env: TOXENV=pypy3
-      dist: trusty
 
 install:
   - pip install tox
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 00ce4dc..b693ba7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,10 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - Use current working directory to find `.env` when bundled by PyInstaller (#213 by
   [@gergelyk]).
 
+### Fixed
+
+- Fix escaping of quoted values written by `set_key` (#236 by [@bbc2]).
+
 ## [0.11.0] - 2020-02-07
 
 ### Added
diff --git a/src/dotenv/main.py b/src/dotenv/main.py
index ce83155..93d617d 100644
--- a/src/dotenv/main.py
+++ b/src/dotenv/main.py
@@ -153,8 +153,11 @@ def set_key(dotenv_path, key_to_set, value_to_set, quote_mode="always"):
     if " " in value_to_set:
         quote_mode = "always"
 
-    line_template = '{}="{}"\n' if quote_mode == "always" else '{}={}\n'
-    line_out = line_template.format(key_to_set, value_to_set)
+    if quote_mode == "always":
+        value_out = '"{}"'.format(value_to_set.replace('"', '\\"'))
+    else:
+        value_out = value_to_set
+    line_out = "{}={}\n".format(key_to_set, value_out)
 
     with rewrite(dotenv_path) as (source, dest):
         replaced = False

set_key doesn't escape values with quotes in them
Suppose you have this code:

```python
import dotenv
import json

key = "KEY_TO_SAVE"
value = json.dumps({"test": "me"})
dotenv.set_key(".env", key, value)
```

What I would expect to see is a `.env` file that looks like this:

```
KEY_TO_SAVE="{\"test\": \"me\"}"
```

However what I actually see is this:

```
KEY_TO_SAVE="{"test": "me"}"
```

Because of the lack of escaping quote character, dotenv has effectively changed the value I was trying to set. In this case, the value passed to dotenv is valid JSON, but the value dotenv saves to the file is not valid JSON.
diff --git a/tests/test_main.py b/tests/test_main.py
index 3416e2c..a4fb5b4 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -29,14 +29,25 @@ def test_set_key_no_file(tmp_path):
     )
 
 
-def test_set_key_new(dotenv_file):
+@pytest.mark.parametrize(
+    "key,value,expected,content",
+    [
+        ("a", "", (True, "a", ""), 'a=""\n'),
+        ("a", "b", (True, "a", "b"), 'a="b"\n'),
+        ("a", "'b'", (True, "a", "b"), 'a="b"\n'),
+        ("a", "\"b\"", (True, "a", "b"), 'a="b"\n'),
+        ("a", "b'c", (True, "a", "b'c"), 'a="b\'c"\n'),
+        ("a", "b\"c", (True, "a", "b\"c"), 'a="b\\\"c"\n'),
+    ],
+)
+def test_set_key_new(dotenv_file, key, value, expected, content):
     logger = logging.getLogger("dotenv.main")
 
     with mock.patch.object(logger, "warning") as mock_warning:
-        result = dotenv.set_key(dotenv_file, "foo", "bar")
+        result = dotenv.set_key(dotenv_file, key, value)
 
-    assert result == (True, "foo", "bar")
-    assert open(dotenv_file, "r").read() == 'foo="bar"\n'
+    assert result == expected
+    assert open(dotenv_file, "r").read() == content
     mock_warning.assert_not_called()
 
 

pip install -e .
parse_log_pytest
requirements.txt
flake8 pytest
3.8
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
asttokens==3.0.0
backcall==0.2.0
backports.tarfile==1.2.0
bump2version==1.0.1
bumpversion==0.6.0
cachetools==5.5.2
certifi==2025.1.31
cffi==1.17.1
chardet==5.2.0
charset-normalizer==3.4.1
click==8.1.8
colorama==0.4.6
coverage==7.6.1
cryptography==44.0.2
decorator==5.2.1
distlib==0.3.9
docstring_parser==0.16
docutils==0.20.1
exceptiongroup==1.2.2
executing==2.2.0
falcon==2.0.0
filelock==3.16.1
flake8==7.1.2
ghp-import==2.1.0
gitdb==4.0.12
GitPython==3.1.44
hug==2.6.1
id==1.5.0
idna==3.10
importlib_metadata==8.5.0
importlib_resources==6.4.5
iniconfig==2.1.0
ipython==8.12.3
jaraco.classes==3.4.0
jaraco.context==6.0.1
jaraco.functools==4.1.0
jedi==0.19.2
jeepney==0.9.0
Jinja2==3.1.6
keyring==25.5.0
livereload==2.7.1
Mako==1.3.9
Markdown==3.3.7
markdown-it-py==3.0.0
MarkupSafe==2.1.5
matplotlib-inline==0.1.7
mccabe==0.7.0
mdurl==0.1.2
mergedeep==1.3.4
mkdocs==1.3.1
mkdocs-material==8.5.4
mkdocs-material-extensions==1.3.1
mock==5.2.0
more-itertools==10.5.0
nh3==0.2.21
packaging==24.2
parso==0.8.4
pdocs==1.2.0
pexpect==4.9.0
pickleshare==0.7.5
platformdirs==4.3.6
pluggy==1.5.0
portray==1.8.0
prompt_toolkit==3.0.50
ptyprocess==0.7.0
pure_eval==0.2.3
pycodestyle==2.12.1
pycparser==2.22
pyflakes==3.2.0
Pygments==2.19.1
pymdown-extensions==10.4
pyproject-api==1.8.0
pytest==8.3.5
pytest-cov==5.0.0
python-dateutil==2.9.0.post0
-e git+https://github.com/theskumar/python-dotenv.git@ba5a16e0e349a6a1f26927c5f833a95c4c645647#egg=python_dotenv
PyYAML==6.0.2
pyyaml_env_tag==0.1
readme_renderer==43.0
requests==2.32.3
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==14.0.0
SecretStorage==3.3.3
sh==2.2.2
six==1.17.0
smmap==5.0.2
stack-data==0.6.3
termcolor==2.4.0
toml==0.10.2
tomli==2.2.1
tornado==6.4.2
tox==4.25.0
traitlets==5.14.3
twine==6.1.0
typing_extensions==4.13.0
urllib3==2.2.3
virtualenv==20.29.3
watchdog==4.0.2
wcwidth==0.2.13
yaspin==2.5.0
zipp==3.20.2

name: python-dotenv
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=24.2=py38h06a4308_0
  - python=3.8.20=he870216_0
  - readline=8.2=h5eee18b_0
  - setuptools=75.1.0=py38h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.44.0=py38h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - asttokens==3.0.0
      - backcall==0.2.0
      - backports-tarfile==1.2.0
      - bump2version==1.0.1
      - bumpversion==0.6.0
      - cachetools==5.5.2
      - certifi==2025.1.31
      - cffi==1.17.1
      - chardet==5.2.0
      - charset-normalizer==3.4.1
      - click==8.1.8
      - colorama==0.4.6
      - coverage==7.6.1
      - cryptography==44.0.2
      - decorator==5.2.1
      - distlib==0.3.9
      - docstring-parser==0.16
      - docutils==0.20.1
      - exceptiongroup==1.2.2
      - executing==2.2.0
      - falcon==2.0.0
      - filelock==3.16.1
      - flake8==7.1.2
      - ghp-import==2.1.0
      - gitdb==4.0.12
      - gitpython==3.1.44
      - hug==2.6.1
      - id==1.5.0
      - idna==3.10
      - importlib-metadata==8.5.0
      - importlib-resources==6.4.5
      - iniconfig==2.1.0
      - ipython==8.12.3
      - jaraco-classes==3.4.0
      - jaraco-context==6.0.1
      - jaraco-functools==4.1.0
      - jedi==0.19.2
      - jeepney==0.9.0
      - jinja2==3.1.6
      - keyring==25.5.0
      - livereload==2.7.1
      - mako==1.3.9
      - markdown==3.3.7
      - markdown-it-py==3.0.0
      - markupsafe==2.1.5
      - matplotlib-inline==0.1.7
      - mccabe==0.7.0
      - mdurl==0.1.2
      - mergedeep==1.3.4
      - mkdocs==1.3.1
      - mkdocs-material==8.5.4
      - mkdocs-material-extensions==1.3.1
      - mock==5.2.0
      - more-itertools==10.5.0
      - nh3==0.2.21
      - packaging==24.2
      - parso==0.8.4
      - pdocs==1.2.0
      - pexpect==4.9.0
      - pickleshare==0.7.5
      - platformdirs==4.3.6
      - pluggy==1.5.0
      - portray==1.8.0
      - prompt-toolkit==3.0.50
      - ptyprocess==0.7.0
      - pure-eval==0.2.3
      - pycodestyle==2.12.1
      - pycparser==2.22
      - pyflakes==3.2.0
      - pygments==2.19.1
      - pymdown-extensions==10.4
      - pyproject-api==1.8.0
      - pytest==8.3.5
      - pytest-cov==5.0.0
      - python-dateutil==2.9.0.post0
      - python-dotenv==0.11.0
      - pyyaml==6.0.2
      - pyyaml-env-tag==0.1
      - readme-renderer==43.0
      - requests==2.32.3
      - requests-toolbelt==1.0.0
      - rfc3986==2.0.0
      - rich==14.0.0
      - secretstorage==3.3.3
      - sh==2.2.2
      - six==1.17.0
      - smmap==5.0.2
      - stack-data==0.6.3
      - termcolor==2.4.0
      - toml==0.10.2
      - tomli==2.2.1
      - tornado==6.4.2
      - tox==4.25.0
      - traitlets==5.14.3
      - twine==6.1.0
      - typing-extensions==4.13.0
      - urllib3==2.2.3
      - virtualenv==20.29.3
      - watchdog==4.0.2
      - wcwidth==0.2.13
      - yaspin==2.5.0
      - zipp==3.20.2
prefix: /opt/conda/envs/python-dotenv

tests/test_main.py::test_set_key_new[a-b"c-expected5-a="b\\"c"\n]
tests/test_main.py::test_set_key_permission_error
tests/test_main.py::test_set_key_no_file tests/test_main.py::test_set_key_new[a--expected0-a=""\n] tests/test_main.py::test_set_key_new[a-b-expected1-a="b"\n] tests/test_main.py::test_set_key_new[a-'b'-expected2-a="b"\n] tests/test_main.py::test_set_key_new[a-"b"-expected3-a="b"\n] tests/test_main.py::test_set_key_new[a-b'c-expected4-a="b'c"\n] tests/test_main.py::test_set_key_new_with_other_values tests/test_main.py::test_set_key_existing tests/test_main.py::test_set_key_existing_with_other_values tests/test_main.py::test_get_key_no_file tests/test_main.py::test_get_key_not_found tests/test_main.py::test_get_key_ok tests/test_main.py::test_get_key_none tests/test_main.py::test_unset_with_value tests/test_main.py::test_unset_no_value tests/test_main.py::test_unset_non_existent_file tests/test_main.py::test_find_dotenv_no_file_raise tests/test_main.py::test_find_dotenv_no_file_no_raise tests/test_main.py::test_find_dotenv_found tests/test_main.py::test_load_dotenv_existing_file tests/test_main.py::test_load_dotenv_no_file_verbose tests/test_main.py::test_load_dotenv_existing_variable_no_override tests/test_main.py::test_load_dotenv_existing_variable_override tests/test_main.py::test_load_dotenv_utf_8 tests/test_main.py::test_load_dotenv_in_current_dir tests/test_main.py::test_dotenv_values_file tests/test_main.py::test_dotenv_values_stream[env0-a=$b-False-expected0] tests/test_main.py::test_dotenv_values_stream[env1-a=$b-True-expected1] tests/test_main.py::test_dotenv_values_stream[env2-a=${b}-False-expected2] tests/test_main.py::test_dotenv_values_stream[env3-a=${b}-True-expected3] tests/test_main.py::test_dotenv_values_stream[env4-b=c\na=${b}-True-expected4] tests/test_main.py::test_dotenv_values_stream[env5-a=${b}-True-expected5] tests/test_main.py::test_dotenv_values_stream[env6-a="${b}"-True-expected6] tests/test_main.py::test_dotenv_values_stream[env7-a='${b}'-True-expected7] tests/test_main.py::test_dotenv_values_stream[env8-a=${a}-True-expected8] tests/test_main.py::test_dotenv_values_stream[env9-a=${a}-True-expected9]
.travis.yml src/dotenv/main.py CHANGELOG.md
.travis.yml src/dotenv/main.py CHANGELOG.md