diff --git a/.travis.yml b/.travis.yml
index bf95c42..9ea281a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -26,14 +26,14 @@ jobs:
     - stage: packaging
       python: "3.6"
       install:
-        - "pip install --upgrade check-manifest readme-renderer"
+        - "pip install --upgrade twine readme-renderer[md]"
       script:
-        - "python setup.py check -m -r -s"
+        - "python setup.py sdist"
+        - "twine check dist/*"
     - stage: coverage
       python: "3.6"
       install:
-        - "pip install --upgrade pip pipenv"
+        - "pip install --upgrade pip pipenv pytest-cov pytest-xdist pytest-timeout pytest"
         - "pipenv install --dev"
-        - "pipenv run pip install --upgrade -e .[spinner,tests]"
       script:
         - "pipenv run pytest -n auto --timeout 300 --cov=vistir --cov-report=term-missing --cov-report=xml --cov-report=html tests"
diff --git a/Pipfile b/Pipfile
index 28bcc75..030516d 100644
--- a/Pipfile
+++ b/Pipfile
@@ -1,12 +1,13 @@
 [packages]
-vistir = { path = '.', editable = true, extras = ["tests"] }
+vistir = {editable = true,extras = ["spinner", "tests"], path = "."}
+coverage = "<5.0"
 
 [dev-packages]
 invoke = '*'
 flake8 = '*'
 parver = '*'
 rope = '*'
-black = {version = "*", markers = "python_version>='3.6'"}
+black = {version = "*",markers = "python_version>='3.6'"}
 towncrier = '*'
 wheel = '*'
 pytest = '*'
diff --git a/Pipfile.lock b/Pipfile.lock
index 379a230..6080e93 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "e68fc781d77e58db9637a9e1f3f4cda87bf90162be4ac8a7d258701be02af310"
+            "sha256": "cc134798bf8133d54b0794d53e64adb68959c205679121f61735930750a4e261"
         },
         "pipfile-spec": 6,
         "requires": {},
@@ -65,42 +65,53 @@
         },
         "colorama": {
             "hashes": [
-                "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda",
-                "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"
+                "sha256:a3d89af5db9e9806a779a50296b5fdb466e281147c2c235e8225ecc6dbf7bbf3",
+                "sha256:c9b54bebe91a6a803e0772c8561d53f2926bfeb17cd141fbabcb08424086595c"
             ],
-            "version": "==0.3.9"
+            "version": "==0.4.0"
         },
         "coverage": {
             "hashes": [
-                "sha256:043d55226aec1d2baf4b2fcab5c204561ccf184a388096f41e396c1c092aff38",
-                "sha256:10bfd0b80b01d0684f968abbe1186bc19962e07b4b7601bb43b175b617cf689d",
-                "sha256:17e59864f19b3233032edb0566f26c25cc7f599503fb34d2645b5ce1fd6c2c3c",
-                "sha256:2105ee183c51fed27e2b6801029b3903f5c2774c78e3f53bd920ca468d0f5679",
-                "sha256:236505d15af6c7b7bfe2a9485db4b2bdea21d9239351483326184314418c79a8",
-                "sha256:237284425271db4f30d458b355decf388ab20b05278bdf8dc9a65de0973726c6",
-                "sha256:26d8eea4c840b73c61a1081d68bceb57b21a2d4f7afda6cac8ac38cb05226b00",
-                "sha256:39a3740f7721155f4269aedf67b211101c07bd2111b334dfd69b807156ab15d9",
-                "sha256:4bd0c42db8efc8a60965769796d43a5570906a870bc819f7388860aa72779d1b",
-                "sha256:4dcddadea47ac30b696956bd18365cd3a86724821656601151e263b86d34798f",
-                "sha256:51ea341289ac4456db946a25bd644f5635e5ae3793df262813cde875887d25c8",
-                "sha256:5415cafb082dad78935b3045c2e5d8907f436d15ad24c3fdb8e1839e084e4961",
-                "sha256:5631f1983074b33c35dbb84607f337b9d7e9808116d7f0f2cb7b9d6d4381d50e",
-                "sha256:5e9249bc361cd22565fd98590a53fd25a3dd666b74791ed7237fa99de938bbed",
-                "sha256:6a48746154f1331f28ef9e889c625b5b15a36cb86dd8021b4bdd1180a2186aa5",
-                "sha256:71d376dbac64855ed693bc1ca121794570fe603e8783cdfa304ec6825d4e768f",
-                "sha256:749ebd8a615337747592bd1523dfc4af7199b2bf6403b55f96c728668aeff91f",
-                "sha256:8ec528b585b95234e9c0c31dcd0a89152d8ed82b4567aa62dbcb3e9a0600deee",
-                "sha256:a1a9ccd879811437ca0307c914f136d6edb85bd0470e6d4966c6397927bcabd9",
-                "sha256:abd956c334752776230b779537d911a5a12fcb69d8fd3fe332ae63a140301ae6",
-                "sha256:ad18f836017f2e8881145795f483636564807aaed54223459915a0d4735300cf",
-                "sha256:b07ac0b1533298ddbc54c9bf3464664895f22899fec027b8d6c8d3ac59023283",
-                "sha256:d9385f1445e30e8e42b75a36a7899ea1fd0f5784233a626625d70f9b087de404",
-                "sha256:db2d1fcd32dbeeb914b2660af1838e9c178b75173f95fd221b1f9410b5d3ef1d",
-                "sha256:e1dec211147f1fd7cb7a0f9a96aeeca467a5af02d38911307b3b8c2324f9917e",
-                "sha256:e96dffc1fa57bb8c1c238f3d989341a97302492d09cb11f77df031112621c35c",
-                "sha256:ed4d97eb0ecdee29d0748acd84e6380729f78ce5ba0c7fe3401801634c25a1c5"
-            ],
-            "version": "==5.0a3"
+                "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f",
+                "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe",
+                "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d",
+                "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0",
+                "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607",
+                "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d",
+                "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b",
+                "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3",
+                "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e",
+                "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815",
+                "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36",
+                "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1",
+                "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14",
+                "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c",
+                "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794",
+                "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b",
+                "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840",
+                "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd",
+                "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82",
+                "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952",
+                "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389",
+                "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f",
+                "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4",
+                "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da",
+                "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647",
+                "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d",
+                "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42",
+                "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478",
+                "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b",
+                "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb",
+                "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9"
+            ],
+            "index": "pypi",
+            "version": "==4.5.2"
+        },
+        "cursor": {
+            "hashes": [
+                "sha256:8ee9fe5b925e1001f6ae6c017e93682583d2b4d1ef7130a26cfcdf1651c0032c"
+            ],
+            "version": "==1.2.0"
         },
         "enum34": {
             "hashes": [
@@ -180,10 +191,10 @@
         },
         "pytest": {
             "hashes": [
-                "sha256:3f193df1cfe1d1609d4c583838bea3d532b18d6160fd3f55c9447fdca30848ec",
-                "sha256:e246cf173c01169b9617fc07264b7b1316e78d7a650055235d6d897bc80d9660"
+                "sha256:488c842647bbeb350029da10325cb40af0a9c7a2fdda45aeb1dda75b60048ffb",
+                "sha256:c055690dfefa744992f563e8c3a654089a6aa5b8092dded9b6fafbd70b2e45a7"
             ],
-            "version": "==3.10.1"
+            "version": "==4.0.0"
         },
         "pytest-cov": {
             "hashes": [
@@ -201,10 +212,10 @@
         },
         "pytest-timeout": {
             "hashes": [
-                "sha256:1117fc0536e1638862917efbdc0895e6b62fa61e6cf4f39bb655686af7af9627",
-                "sha256:b050a05da96a9992e90e884bc19b4790678b40c25471d2b77015b388417e1fa8"
+                "sha256:4a30ba76837a32c7b7cd5c84ee9933fde4b9022b0cd20ea7d4a577c2a1649fb1",
+                "sha256:d49f618c6448c14168773b6cdda022764c63ea80d42274e3156787e8088d04c6"
             ],
-            "version": "==1.3.2"
+            "version": "==1.3.3"
         },
         "pytest-xdist": {
             "hashes": [
@@ -349,10 +360,10 @@
         },
         "colorama": {
             "hashes": [
-                "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda",
-                "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"
+                "sha256:a3d89af5db9e9806a779a50296b5fdb466e281147c2c235e8225ecc6dbf7bbf3",
+                "sha256:c9b54bebe91a6a803e0772c8561d53f2926bfeb17cd141fbabcb08424086595c"
             ],
-            "version": "==0.3.9"
+            "version": "==0.4.0"
         },
         "configparser": {
             "hashes": [
@@ -363,35 +374,46 @@
         },
         "coverage": {
             "hashes": [
-                "sha256:043d55226aec1d2baf4b2fcab5c204561ccf184a388096f41e396c1c092aff38",
-                "sha256:10bfd0b80b01d0684f968abbe1186bc19962e07b4b7601bb43b175b617cf689d",
-                "sha256:17e59864f19b3233032edb0566f26c25cc7f599503fb34d2645b5ce1fd6c2c3c",
-                "sha256:2105ee183c51fed27e2b6801029b3903f5c2774c78e3f53bd920ca468d0f5679",
-                "sha256:236505d15af6c7b7bfe2a9485db4b2bdea21d9239351483326184314418c79a8",
-                "sha256:237284425271db4f30d458b355decf388ab20b05278bdf8dc9a65de0973726c6",
-                "sha256:26d8eea4c840b73c61a1081d68bceb57b21a2d4f7afda6cac8ac38cb05226b00",
-                "sha256:39a3740f7721155f4269aedf67b211101c07bd2111b334dfd69b807156ab15d9",
-                "sha256:4bd0c42db8efc8a60965769796d43a5570906a870bc819f7388860aa72779d1b",
-                "sha256:4dcddadea47ac30b696956bd18365cd3a86724821656601151e263b86d34798f",
-                "sha256:51ea341289ac4456db946a25bd644f5635e5ae3793df262813cde875887d25c8",
-                "sha256:5415cafb082dad78935b3045c2e5d8907f436d15ad24c3fdb8e1839e084e4961",
-                "sha256:5631f1983074b33c35dbb84607f337b9d7e9808116d7f0f2cb7b9d6d4381d50e",
-                "sha256:5e9249bc361cd22565fd98590a53fd25a3dd666b74791ed7237fa99de938bbed",
-                "sha256:6a48746154f1331f28ef9e889c625b5b15a36cb86dd8021b4bdd1180a2186aa5",
-                "sha256:71d376dbac64855ed693bc1ca121794570fe603e8783cdfa304ec6825d4e768f",
-                "sha256:749ebd8a615337747592bd1523dfc4af7199b2bf6403b55f96c728668aeff91f",
-                "sha256:8ec528b585b95234e9c0c31dcd0a89152d8ed82b4567aa62dbcb3e9a0600deee",
-                "sha256:a1a9ccd879811437ca0307c914f136d6edb85bd0470e6d4966c6397927bcabd9",
-                "sha256:abd956c334752776230b779537d911a5a12fcb69d8fd3fe332ae63a140301ae6",
-                "sha256:ad18f836017f2e8881145795f483636564807aaed54223459915a0d4735300cf",
-                "sha256:b07ac0b1533298ddbc54c9bf3464664895f22899fec027b8d6c8d3ac59023283",
-                "sha256:d9385f1445e30e8e42b75a36a7899ea1fd0f5784233a626625d70f9b087de404",
-                "sha256:db2d1fcd32dbeeb914b2660af1838e9c178b75173f95fd221b1f9410b5d3ef1d",
-                "sha256:e1dec211147f1fd7cb7a0f9a96aeeca467a5af02d38911307b3b8c2324f9917e",
-                "sha256:e96dffc1fa57bb8c1c238f3d989341a97302492d09cb11f77df031112621c35c",
-                "sha256:ed4d97eb0ecdee29d0748acd84e6380729f78ce5ba0c7fe3401801634c25a1c5"
-            ],
-            "version": "==5.0a3"
+                "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f",
+                "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe",
+                "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d",
+                "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0",
+                "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607",
+                "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d",
+                "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b",
+                "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3",
+                "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e",
+                "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815",
+                "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36",
+                "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1",
+                "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14",
+                "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c",
+                "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794",
+                "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b",
+                "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840",
+                "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd",
+                "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82",
+                "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952",
+                "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389",
+                "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f",
+                "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4",
+                "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da",
+                "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647",
+                "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d",
+                "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42",
+                "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478",
+                "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b",
+                "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb",
+                "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9"
+            ],
+            "index": "pypi",
+            "version": "==4.5.2"
+        },
+        "cursor": {
+            "hashes": [
+                "sha256:8ee9fe5b925e1001f6ae6c017e93682583d2b4d1ef7130a26cfcdf1651c0032c"
+            ],
+            "version": "==1.2.0"
         },
         "docutils": {
             "hashes": [
@@ -600,10 +622,10 @@
         },
         "pytest": {
             "hashes": [
-                "sha256:3f193df1cfe1d1609d4c583838bea3d532b18d6160fd3f55c9447fdca30848ec",
-                "sha256:e246cf173c01169b9617fc07264b7b1316e78d7a650055235d6d897bc80d9660"
+                "sha256:488c842647bbeb350029da10325cb40af0a9c7a2fdda45aeb1dda75b60048ffb",
+                "sha256:c055690dfefa744992f563e8c3a654089a6aa5b8092dded9b6fafbd70b2e45a7"
             ],
-            "version": "==3.10.1"
+            "version": "==4.0.0"
         },
         "pytest-cov": {
             "hashes": [
@@ -621,10 +643,10 @@
         },
         "pytest-timeout": {
             "hashes": [
-                "sha256:1117fc0536e1638862917efbdc0895e6b62fa61e6cf4f39bb655686af7af9627",
-                "sha256:b050a05da96a9992e90e884bc19b4790678b40c25471d2b77015b388417e1fa8"
+                "sha256:4a30ba76837a32c7b7cd5c84ee9933fde4b9022b0cd20ea7d4a577c2a1649fb1",
+                "sha256:d49f618c6448c14168773b6cdda022764c63ea80d42274e3156787e8088d04c6"
             ],
-            "version": "==1.3.2"
+            "version": "==1.3.3"
         },
         "pytest-xdist": {
             "hashes": [
@@ -739,6 +761,14 @@
             ],
             "version": "==1.24.1"
         },
+        "vistir": {
+            "editable": true,
+            "extras": [
+                "tests",
+                "spinner"
+            ],
+            "path": "."
+        },
         "wheel": {
             "hashes": [
                 "sha256:9fa1f772f1a2df2bd00ddb4fa57e1cc349301e1facb98fbe62329803a9ff1196",
@@ -746,6 +776,13 @@
             ],
             "index": "pypi",
             "version": "==0.32.1"
+        },
+        "yaspin": {
+            "hashes": [
+                "sha256:36fdccc5e0637b5baa8892fe2c3d927782df7d504e9020f40eb2c1502518aa5a",
+                "sha256:8e52bf8079a48e2a53f3dfeec9e04addb900c101d1591c85df69cf677d3237e7"
+            ],
+            "version": "==0.14.0"
         }
     }
 }
diff --git a/news/40.feature.rst b/news/40.feature.rst
new file mode 100644
index 0000000..f50507f
--- /dev/null
+++ b/news/40.feature.rst
@@ -0,0 +1,1 @@
+Added the ability to always write spinner output to stderr using ``write_to_stdout=False``.
diff --git a/src/vistir/contextmanagers.py b/src/vistir/contextmanagers.py
index 0920a9c..77fbb9d 100644
--- a/src/vistir/contextmanagers.py
+++ b/src/vistir/contextmanagers.py
@@ -103,13 +103,14 @@ def dummy_spinner(spin_type, text, **kwargs):
 
 
 @contextmanager
-def spinner(spinner_name=None, start_text=None, handler_map=None, nospin=False):
+def spinner(spinner_name=None, start_text=None, handler_map=None, nospin=False, write_to_stdout=True):
     """Get a spinner object or a dummy spinner to wrap a context.
 
     :param str spinner_name: A spinner type e.g. "dots" or "bouncingBar" (default: {"bouncingBar"})
     :param str start_text: Text to start off the spinner with (default: {None})
     :param dict handler_map: Handler map for signals to be handled gracefully (default: {None})
     :param bool nospin: If true, use the dummy spinner (default: {False})
+    :param bool write_to_stdout: Writes to stdout if true, otherwise writes to stderr (default: True)
     :return: A spinner object which can be manipulated while alive
     :rtype: :class:`~vistir.spin.VistirSpinner`
 
@@ -136,14 +137,15 @@ def spinner(spinner_name=None, start_text=None, handler_map=None, nospin=False):
     use_yaspin = (has_yaspin is False) or (nospin is True)
     if has_yaspin is None or has_yaspin is True and not nospin:
         use_yaspin = True
-    if not start_text and nospin is False:
+    if start_text is None and use_yaspin is True:
         start_text = "Running..."
     with create_spinner(
         spinner_name=spinner_name,
         text=start_text,
         handler_map=handler_map,
         nospin=nospin,
-        use_yaspin=use_yaspin
+        use_yaspin=use_yaspin,
+        write_to_stdout=write_to_stdout
     ) as _spinner:
         yield _spinner
 
diff --git a/src/vistir/misc.py b/src/vistir/misc.py
index a9a127d..66dade3 100644
--- a/src/vistir/misc.py
+++ b/src/vistir/misc.py
@@ -106,7 +106,7 @@ def _spawn_subprocess(script, env=None, block=True, cwd=None, combine_stderr=Tru
     from distutils.spawn import find_executable
 
     if not env:
-        env = {}
+        env = os.environ.copy()
     command = find_executable(script.command)
     options = {
         "env": env,
@@ -148,15 +148,16 @@ def _create_subprocess(
     spinner=None,
     combine_stderr=False,
     display_limit=200,
-    start_text=""
+    start_text="",
+    write_to_stdout=True
 ):
     if not env:
-        env = {}
+        env = os.environ.copy()
     try:
         c = _spawn_subprocess(cmd, env=env, block=block, cwd=cwd,
-                                                    combine_stderr=combine_stderr)
+                              combine_stderr=combine_stderr)
     except Exception as exc:
-        print("Error %s while executing command %s", exc, " ".join(cmd._parts))
+        sys.stderr.write("Error %s while executing command %s", exc, " ".join(cmd._parts))
         raise
     if not block:
         c.stdin.close()
@@ -193,9 +194,7 @@ def _create_subprocess(
                 err_line = fs_str("{0}".format(stderr_line))
                 if verbose and err_line is not None:
                     if spinner:
-                        spinner._hide_cursor()
-                        spinner.write_err(err_line)
-                        spinner._show_cursor()
+                        spinner.hide_and_write(err_line, target=spinner.stderr)
                     else:
                         sys.stderr.write(err_line)
                         sys.stderr.flush()
@@ -206,12 +205,12 @@ def _create_subprocess(
                     display_line = "{0}...".format(stdout_line[:display_limit])
                 if verbose and display_line is not None:
                     if spinner:
-                        spinner._hide_cursor()
-                        spinner.write_err(display_line)
-                        spinner._show_cursor()
+                        target = spinner.stdout if write_to_stdout else spinner.stderr
+                        spinner.hide_and_write(display_line, target=target)
                     else:
-                        sys.stderr.write(display_line)
-                        sys.stderr.flush()
+                        target = sys.stdout if write_to_stdout else sys.stderr
+                        target.write(display_line)
+                        target.flush()
                 if spinner:
                     spinner.text = to_native_string("{0} {1}".format(spinner_orig_text, display_line))
                 continue
@@ -252,7 +251,8 @@ def run(
     nospin=False,
     spinner_name=None,
     combine_stderr=True,
-    display_limit=200
+    display_limit=200,
+    write_to_stdout=True
 ):
     """Use `subprocess.Popen` to get the output of a command and decode it.
 
@@ -266,6 +266,7 @@ def run(
     :param str spinner_name: The name of the spinner to use if enabled, defaults to bouncingBar
     :param bool combine_stderr: Optionally merge stdout and stderr in the subprocess, false if nonblocking.
     :param int dispay_limit: The max width of output lines to display when using a spinner.
+    :param bool write_to_stdout: Whether to write to stdout when using a spinner, default True.
     :returns: A 2-tuple of (output, error) or a :class:`subprocess.Popen` object.
 
     .. Warning:: Merging standard out and standarad error in a nonblocking subprocess
@@ -296,7 +297,8 @@ def run(
     if block or not return_object:
         combine_stderr = False
     start_text = ""
-    with spinner(spinner_name=spinner_name, start_text=start_text, nospin=nospin) as sp:
+    with spinner(spinner_name=spinner_name, start_text=start_text, nospin=nospin,
+                 write_to_stdout=write_to_stdout) as sp:
         return _create_subprocess(
             cmd,
             env=_env,
@@ -306,7 +308,8 @@ def run(
             verbose=verbose,
             spinner=sp,
             combine_stderr=combine_stderr,
-            start_text=start_text
+            start_text=start_text,
+            write_to_stdout=True
         )
 
 
@@ -323,7 +326,7 @@ def load_path(python):
 
     python = Path(python).as_posix()
     out, err = run([python, "-c", "import json, sys; print(json.dumps(sys.path))"],
-                        nospin=True)
+                   nospin=True)
     if out:
         return json.loads(out)
     else:
diff --git a/src/vistir/spin.py b/src/vistir/spin.py
index e731155..5e975b6 100644
--- a/src/vistir/spin.py
+++ b/src/vistir/spin.py
@@ -4,6 +4,8 @@ import functools
 import os
 import signal
 import sys
+import threading
+import time
 
 import colorama
 import cursor
@@ -34,14 +36,16 @@ class DummySpinner(object):
     def __init__(self, text="", **kwargs):
         colorama.init()
         from .misc import decode_for_output
-        self.text = to_native_string(decode_for_output(text))
+        self.text = to_native_string(decode_for_output(text)) if text else ""
         self.stdout = kwargs.get("stdout", sys.stdout)
         self.stderr = kwargs.get("stderr", sys.stderr)
         self.out_buff = StringIO()
+        self.write_to_stdout = kwargs.get("write_to_stdout", False)
 
     def __enter__(self):
         if self.text and self.text != "None":
-            self.write_err(self.text)
+            if self.write_to_stdout:
+                self.write(self.text)
         return self
 
     def __exit__(self, exc_type, exc_val, traceback):
@@ -72,16 +76,36 @@ class DummySpinner(object):
     def fail(self, exitcode=1, text="FAIL"):
         from .misc import decode_for_output
         if text and text != "None":
-            self.write_err(decode_for_output(text))
+            if self.write_to_stdout:
+                self.write(decode_for_output(text))
+            else:
+                self.write_err(decode_for_output(text))
         self._close_output_buffer()
 
     def ok(self, text="OK"):
         if text and text != "None":
-            self.stderr.write(self.text)
+            if self.write_to_stdout:
+                self.stdout.write(self.text)
+            else:
+                self.stderr.write(self.text)
         self._close_output_buffer()
         return 0
 
+    def hide_and_write(self, text, target=None):
+        if not target:
+            target = self.stdout
+        from .misc import decode_for_output
+        if text is None or isinstance(text, six.string_types) and text == "None":
+            pass
+        target.write(decode_for_output("\r"))
+        self._hide_cursor(target=target)
+        target.write(decode_for_output("{0}\n".format(text)))
+        target.write(CLEAR_LINE)
+        self._show_cursor(target=target)
+
     def write(self, text=None):
+        if not self.write_to_stdout:
+            return self.write_err(text)
         from .misc import decode_for_output
         if text is None or isinstance(text, six.string_types) and text == "None":
             pass
@@ -102,11 +126,11 @@ class DummySpinner(object):
         self.stderr.write(CLEAR_LINE)
 
     @staticmethod
-    def _hide_cursor():
+    def _hide_cursor(target=None):
         pass
 
     @staticmethod
-    def _show_cursor():
+    def _show_cursor(target=None):
         pass
 
 
@@ -114,14 +138,18 @@ base_obj = yaspin.core.Yaspin if yaspin is not None else DummySpinner
 
 
 class VistirSpinner(base_obj):
+    "A spinner class for handling spinners on windows and posix."
+
     def __init__(self, *args, **kwargs):
-        """Get a spinner object or a dummy spinner to wrap a context.
+        """
+        Get a spinner object or a dummy spinner to wrap a context.
 
         Keyword Arguments:
         :param str spinner_name: A spinner type e.g. "dots" or "bouncingBar" (default: {"bouncingBar"})
         :param str start_text: Text to start off the spinner with (default: {None})
         :param dict handler_map: Handler map for signals to be handled gracefully (default: {None})
         :param bool nospin: If true, use the dummy spinner (default: {False})
+        :param bool write_to_stdout: Writes to stdout if true, otherwise writes to stderr (default: True)
         """
 
         self.handler = handler
@@ -145,36 +173,54 @@ class VistirSpinner(base_obj):
         kwargs["text"] = start_text if start_text is not None else _text
         kwargs["sigmap"] = sigmap
         kwargs["spinner"] = getattr(Spinners, spinner_name, "")
+        write_to_stdout = kwargs.pop("write_to_stdout", True)
         self.stdout = kwargs.pop("stdout", sys.stdout)
         self.stderr = kwargs.pop("stderr", sys.stderr)
         self.out_buff = StringIO()
-        super(VistirSpinner, self).__init__(*args, **kwargs)
+        self.write_to_stdout = write_to_stdout
         self.is_dummy = bool(yaspin is None)
+        super(VistirSpinner, self).__init__(*args, **kwargs)
 
-    def ok(self, text="OK"):
+    def ok(self, text="OK", err=False):
         """Set Ok (success) finalizer to a spinner."""
         # Do not display spin text for ok state
         self._text = None
 
         _text = text if text else "OK"
-        self._freeze(_text)
+        err = err or not self.write_to_stdout
+        self._freeze(_text, err=err)
 
-    def fail(self, text="FAIL"):
+    def fail(self, text="FAIL", err=False):
         """Set fail finalizer to a spinner."""
         # Do not display spin text for fail state
         self._text = None
 
         _text = text if text else "FAIL"
-        self._freeze(_text)
+        err = err or not self.write_to_stdout
+        self._freeze(_text, err=err)
+
+    def hide_and_write(self, text, target=None):
+        if not target:
+            target = self.stdout
+        from .misc import decode_for_output
+        if text is None or isinstance(text, six.string_types) and text == "None":
+            pass
+        target.write(decode_for_output("\r"))
+        self._hide_cursor(target=target)
+        target.write(decode_for_output("{0}\n".format(text)))
+        target.write(CLEAR_LINE)
+        self._show_cursor(target=target)
 
     def write(self, text):
+        if not self.write_to_stdout:
+            return self.write_err(text)
         from .misc import to_text
         sys.stdout.write("\r")
         self.stdout.write(CLEAR_LINE)
         if text is None:
             text = ""
         text = to_native_string("{0}\n".format(text))
-        sys.stdout.write(text)
+        self.stdout.write(text)
         self.out_buff.write(to_text(text))
 
     def write_err(self, text):
@@ -189,7 +235,46 @@ class VistirSpinner(base_obj):
         self.stderr.write(text)
         self.out_buff.write(to_text(text))
 
-    def _freeze(self, final_text):
+    def start(self):
+        if self._sigmap:
+            self._register_signal_handlers()
+
+        target = self.stdout if self.write_to_stdout else self.stderr
+        if target.isatty():
+            self._hide_cursor(target=target)
+
+        self._stop_spin = threading.Event()
+        self._hide_spin = threading.Event()
+        self._spin_thread = threading.Thread(target=self._spin)
+        self._spin_thread.start()
+
+    def stop(self):
+        if self._dfl_sigmap:
+            # Reset registered signal handlers to default ones
+            self._reset_signal_handlers()
+
+        if self._spin_thread:
+            self._stop_spin.set()
+            self._spin_thread.join()
+
+        target = self.stdout if self.write_to_stdout else self.stderr
+        if target.isatty():
+            target.write("\r")
+
+        if self.write_to_stdout:
+            self._clear_line()
+        else:
+            self._clear_err()
+
+        if target.isatty():
+            self._show_cursor(target=target)
+        if self.stderr and self.stderr != sys.stderr:
+            self.stderr.close()
+        if self.stdout and self.stdout != sys.stdout:
+            self.stdout.close()
+        self.out_buff.close()
+
+    def _freeze(self, final_text, err=False):
         """Stop spinner, compose last frame and 'freeze' it."""
         if not final_text:
             final_text = ""
@@ -199,15 +284,10 @@ class VistirSpinner(base_obj):
         # Should be stopped here, otherwise prints after
         # self._freeze call will mess up the spinner
         self.stop()
-        self.stdout.write(self._last_frame)
-
-    def stop(self, *args, **kwargs):
-        if self.stderr and self.stderr != sys.stderr:
-            self.stderr.close()
-        if self.stdout and self.stdout != sys.stdout:
-            self.stdout.close()
-        self.out_buff.close()
-        super(VistirSpinner, self).stop(*args, **kwargs)
+        if err or not self.write_to_stdout:
+            self.stderr.write(self._last_frame)
+        else:
+            self.stdout.write(self._last_frame)
 
     def _compose_color_func(self):
         fn = functools.partial(
@@ -236,6 +316,29 @@ class VistirSpinner(base_obj):
             out = to_native_string("{0} {1}\n".format(frame, text))
         return out
 
+    def _spin(self):
+        target = self.stdout if self.write_to_stdout else self.stderr
+        clear_fn = self._clear_line if self.write_to_stdout else self._clear_err
+        while not self._stop_spin.is_set():
+
+            if self._hide_spin.is_set():
+                # Wait a bit to avoid wasting cycles
+                time.sleep(self._interval)
+                continue
+
+            # Compose output
+            spin_phase = next(self._cycle)
+            out = self._compose_out(spin_phase)
+
+            # Write
+            target.write(out)
+            clear_fn()
+            target.flush()
+
+            # Wait
+            time.sleep(self._interval)
+            target.write("\b")
+
     def _register_signal_handlers(self):
         # SIGKILL cannot be caught or ignored, and the receiving
         # process cannot perform any clean-up upon receiving this
@@ -273,12 +376,16 @@ class VistirSpinner(base_obj):
             signal.signal(sig, sig_handler)
 
     @staticmethod
-    def _hide_cursor():
-        cursor.hide()
+    def _hide_cursor(target=None):
+        if not target:
+            target = sys.stdout
+        cursor.hide(stream=target)
 
     @staticmethod
-    def _show_cursor():
-        cursor.show()
+    def _show_cursor(target=None):
+        if not target:
+            target = sys.stdout
+        cursor.show(stream=target)
 
     @staticmethod
     def _clear_err():
diff --git a/tox.ini b/tox.ini
index b422a0b..d99a612 100644
--- a/tox.ini
+++ b/tox.ini
@@ -31,7 +31,9 @@ commands =
 [testenv:packaging]
 deps =
     check-manifest
-    readme_renderer
+    twine
+    readme_renderer[md]
 commands =
     check-manifest
-    python setup.py check -m -r -s
+    python setup.py sdist bdist_wheel
+    twine check dist/*

Provide option to redirect frozen spinner frame to stderr instead of stdout
The spinner should provide the possibility of freezing the final frame to stderr instead of stdout

Ref: pypa/pipenv#3239
diff --git a/tests/test_spinner.py b/tests/test_spinner.py
new file mode 100644
index 0000000..0a15cd5
--- /dev/null
+++ b/tests/test_spinner.py
@@ -0,0 +1,19 @@
+# -*- coding=utf-8 -*-
+
+import vistir.spin
+import time
+
+
+def test_spinner(capsys):
+    with vistir.spin.create_spinner(spinner_name="bouncingBar", text="Running...", nospin=False) as spinner:
+        time.sleep(3)
+        spinner.ok("Ok!")
+    out, err = capsys.readouterr()
+    assert out.strip().endswith("Ok!")
+    assert err.strip() == ""
+    with vistir.spin.create_spinner(spinner_name="bouncingBar", text="Running...", nospin=False, write_to_stdout=False) as spinner:
+        time.sleep(3)
+        spinner.ok("Ok!")
+    out, err = capsys.readouterr()
+    assert err.strip().endswith("Ok!")
+    assert out.strip() == ""

pip install -e .[spinner,tests]
parse_log_pytest
pytest
pytest
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
certifi==2021.5.30
charset-normalizer==2.0.12
colorama==0.4.5
coverage==6.2
cursor==1.3.5
dataclasses==0.8
execnet==1.9.0
hypothesis==6.31.6
hypothesis-fspaths==0.1
idna==3.10
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pytest==6.2.4
pytest-cov==4.0.0
pytest-timeout==2.1.0
pytest-xdist==3.0.2
requests==2.27.1
six==1.17.0
sortedcontainers==2.4.0
termcolor==1.1.0
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
tomli==1.2.3
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
urllib3==1.26.20
-e git+https://github.com/sarugaku/vistir.git@2c08b5bc8dd5961d3372f3ef8635f95aba2f3e3b#egg=vistir
yaspin==2.1.0
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: vistir
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
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - charset-normalizer==2.0.12
      - colorama==0.4.5
      - coverage==6.2
      - cursor==1.3.5
      - dataclasses==0.8
      - execnet==1.9.0
      - hypothesis==6.31.6
      - hypothesis-fspaths==0.1
      - idna==3.10
      - pytest-cov==4.0.0
      - pytest-timeout==2.1.0
      - pytest-xdist==3.0.2
      - requests==2.27.1
      - six==1.17.0
      - sortedcontainers==2.4.0
      - termcolor==1.1.0
      - tomli==1.2.3
      - urllib3==1.26.20
      - yaspin==2.1.0
prefix: /opt/conda/envs/vistir

tests/test_spinner.py::test_spinner
src/vistir/contextmanagers.py src/vistir/misc.py news/40.feature.rst Pipfile.lock .travis.yml src/vistir/spin.py Pipfile tox.ini
src/vistir/contextmanagers.py src/vistir/misc.py news/40.feature.rst Pipfile.lock .travis.yml src/vistir/spin.py Pipfile tox.ini