diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,188 @@
+# Haskell CPython C bindings [![Build Status](https://travis-ci.org/zsedem/haskell-cpython.svg?branch=main)](https://travis-ci.org/zsedem/haskell-cpython)
+
+This library provides C bindings to more or less all of the python3 C API.
+
+> WARNING: Note that the python 3 C API might be quite stable, BUT the [ABI](https://docs.python.org/3/c-api/stable.html) is not, which
+means if you compiled with a certain minor version (3.7.1) you should run your program
+with the same minor version (3.7.x). (Using docker or nix to package your program is enough
+to avoid this problem)
+
+## Writing a Haskell wrapper over a Python module
+
+The easiest way to get started is to `import CPython.Simple`
+
+The `Simple` API surface is fairly small, so if you're doing something fancy you may need to dip into other parts of `CPython`
+
+### General Info
+
+`initialize :: IO ()` kicks off talking to Python, and will need to be called before using other functions.
+
+The `ToPy` and `FromPy` instances are what let us convert Haskell values to and from the corresponding Python values. There are `easyToPy` and `easyFromPy` helpers to help you easily write your own instances in common cases. If you find an instance for some common Haskell type is missing, please submit a PR!
+
+`Arg` is a type representing an argument to a Python function, which lets us use various Haskell types in the same list of arguments.
+
+```haskell
+sampleArgs :: [Arg]
+sampleArgs =
+  [ arg (7 :: Integer)
+  , arg ("hello" :: Text)
+  ]
+```
+
+### Calling Functions
+
+The most common use case is to call some Python function, which we can do with `call`
+
+```haskell
+call
+  :: FromPy a
+  => Text -- ^ module name
+  -> Text -- ^ function name
+  -> [Arg] -- ^ args
+  -> [(Text, Arg)] -- ^ keyword args
+  -> IO a
+```
+
+For example, if we wanted to wrap Python's `random.randint(low, high)`, we could write this:
+
+```haskell
+randint :: Integer -> Integer -> IO Integer
+randint low high =
+  call "random" "randint" [arg low, arg high] []
+```
+
+Because of the `FromPy` instance in `call`'s type signature, we can infer what to do to convert a Python value back into Haskell, if given the type
+
+```haskell
+uniform :: Integer -> Integer -> IO Double
+uniform low high =
+  call "random" "uniform" [arg low, arg high] []
+```
+
+We can also use the `TypeApplications` language extension to do this, if needed
+
+```haskell
+call @Double "random" "uniform" [arg low, arg high] []
+```
+
+Calling a function with mixed positional and keyword arguments is also fairly straightforward:
+
+```haskell
+moveToDuration :: Integer -> Integer -> Double -> IO ()
+moveToDuration x y seconds =
+  call "pyautogui" "moveTo" [arg x, arg y] [("duration", arg seconds)]
+```
+
+### Getting and Setting Attributes
+
+`getAttribute` lets us get the value of an attribute of some Python module
+
+```haskell
+getAttribute
+  :: FromPy a
+  => Text -- ^ module name
+  -> Text -- ^ attribute name
+  -> IO a
+```
+
+Here, we get the value of `random.BPF`
+
+```haskell
+getBpf :: IO Integer
+getBpf = getAttribute "random" "BPF"
+```
+
+Likewise, `setAttribute` lets us set the value of an attribute
+
+```haskell
+setAttribute
+  :: ToPy a
+  => Text -- ^ module name
+  -> Text -- ^ attribute name
+  -> a -- ^ value to set attribute to
+  -> IO ()
+```
+
+Here's how we can set `random.BPF` to some given number `n`
+
+```haskell
+setBpf :: Integer -> IO ()
+setBpf n = setAttribute "random" "BPF" n
+```
+
+## Using the Low Level API
+
+Sometimes it might be useful to use the less simpler API, especially if you are
+already familiar with the [CPython C API](https://docs.python.org/3/c-api/index.html).
+This API comes with one-on-one connections between the C API methods and the Haskell methods,
+but you won't have to write FFI code directly (like calling incref/decref for Python GC).
+
+After you are familiar with the concepts from the C API, you can search for
+methods in the [API docs](http://hackage.haskell.org/package/cpython-3.5.0) on hackage
+
+These examples below should help you start with using the API, by showing the 
+equivalent haskell code to implement the same as the python example.
+
+### Using `builtins.sum` function
+```haskell
+sumWithPy :: [Integer] -> IO Int
+sumWithPy intlist = do
+    testList <- traverse toObj intlist >>= PyList.toList >>= (return . Py.toObject)
+    builtinsModule <- Py.importModule "builtins"
+    sumFunc <- PyUnicode.toUnicode "sum" >>= Py.getAttribute builtinsModule
+    args <- PyTuple.toTuple [testList]
+    kwargs <- PyDict.new
+    Py.call sumFunc args kwargs >>= castToNumber >>= Py.toInteger >>= PyInt.fromInteger
+  where
+    castToNumber obj = do x <- Py.castToNumber obj
+                          return $ fromMaybe (error "not a number returned from the sum") x
+    toObj integer = fmap Py.toObject $ PyInt.toInteger integer
+```
+This example should show you how different it is to call python from strongly typed code, because you have to
+handle every bit of the errors, like getting an attribute of a module or just creating new python objects.
+
+```python
+intlist = [1, 10, 100, 42]
+sum(intlist)
+```
+### Printing traceback from python
+This example is an approach to handle python exceptions, like python would do it, so if an exception comes, we print a traceback
+```haskell
+{-# LANGUAGE OverloadedStrings #-}
+module Main
+  ( main ) where
+
+import qualified CPython as Py
+import qualified CPython.Types.Module as Py
+import qualified CPython.Types.Dictionary as PyDict
+import qualified CPython.Types.List as PyList
+import qualified CPython.Types.Tuple as PyTuple
+import qualified CPython.Types.Unicode as PyUnicode
+import qualified CPython.Types.Exception as PyExc
+import Data.Text()
+import Control.Exception(handle)
+
+main :: IO ()
+main = handle pyExceptionHandler $ do
+  Py.initialize
+  callingSomePython
+  Py.finalize
+  where
+    pyExceptionHandler :: PyExc.Exception -> IO ()
+    pyExceptionHandler exception = handle pyExceptionHandlerWithoutPythonTraceback $ do
+        tracebackModule <- Py.importModule "traceback"
+        print_exc <- PyUnicode.toUnicode "print_exception" >>= Py.getAttribute tracebackModule
+        kwargs <- PyDict.new
+        args <- case PyExc.exceptionTraceback exception of
+          Just tb -> PyTuple.toTuple [PyExc.exceptionType exception, PyExc.exceptionValue exception, tb]
+          _ -> PyTuple.toTuple [PyExc.exceptionType exception, PyExc.exceptionValue exception]
+        _ <- Py.call print_exc args kwargs
+        return ()
+    pyExceptionHandlerWithoutPythonTraceback :: PyExc.Exception -> IO ()
+    pyExceptionHandlerWithoutPythonTraceback exception = do
+        print exception
+        putStrLn "Unexpected Python exception (Please report a bug)"
+
+callingSomePython :: IO ()
+callingSomePython = do ...
+```
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,8 @@
+# 3.5.1
+
+* add new API for simple usage (See readme for details)
+* improve hackage page
+
 # 3.5.0
 
 * made the compilation compatible with different python3 versions
diff --git a/cpython.cabal b/cpython.cabal
--- a/cpython.cabal
+++ b/cpython.cabal
@@ -1,14 +1,16 @@
 name: cpython
-version: 3.5.0
+version: 3.5.1
 license: GPL-3
 license-file: license.txt
 author: John Millikin <jmillikin@gmail.com>
 maintainer: Adam Zsigmond <zsedem@gmail.com>
 build-type: Simple
-cabal-version: >= 1.10
+cabal-version: 1.18
 category: Foreign
 homepage: https://github.com/zsedem/haskell-cpython
-extra-source-files:  changelog.md
+extra-doc-files:
+  changelog.md
+  README.md
 synopsis: Bindings for libpython
 description:
   These bindings allow Haskell code to call CPython code. It is not
@@ -77,6 +79,8 @@
     CPython.Protocols.Sequence
     CPython.Constants
     CPython.Reflection
+    CPython.Simple
+    CPython.Simple.Instances
     CPython.System
 
   other-modules:
diff --git a/lib/CPython/Simple.hs b/lib/CPython/Simple.hs
new file mode 100644
--- /dev/null
+++ b/lib/CPython/Simple.hs
@@ -0,0 +1,82 @@
+{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module CPython.Simple where
+
+import CPython.Simple.Instances
+
+import Control.Exception (catch, SomeException)
+import Data.Semigroup ((<>))
+import Data.Text (Text)
+import qualified Data.Text as T
+
+import qualified CPython as Py
+import qualified CPython.Protocols.Object as Py
+import qualified CPython.Types as Py
+import qualified CPython.Types.Module as Py
+import qualified CPython.Types.Tuple as Py (toTuple)
+import qualified CPython.Types.Dictionary as PyDict
+
+data Arg = forall a. ToPy a => Arg a
+
+instance ToPy Arg where
+  toPy (Arg a) = toPy a
+
+arg :: ToPy a => a -> Arg
+arg = Arg
+
+initialize :: IO ()
+initialize = Py.initialize
+
+importModule :: Text -> IO Py.Module
+importModule module_ = Py.importModule module_
+
+call
+  :: FromPy a
+  => Text -- ^ module name
+  -> Text -- ^ function name
+  -> [Arg] -- ^ args
+  -> [(Text, Arg)] -- ^ kwargs
+  -> IO a
+call moduleName func args kwargs = do
+  module_ <- importModule moduleName
+  pyFunc <- Py.getAttribute module_ =<< Py.toUnicode func
+  pyArgs <- mapM toPy args
+  pyArgsTuple <- Py.toTuple pyArgs
+  pyKwargs <- toPyKwargs kwargs
+  result <- Py.call pyFunc pyArgsTuple pyKwargs
+  fromPy result
+  where
+    toPyKwargs :: [(Text, Arg)] -> IO Py.Dictionary
+    toPyKwargs dict = do
+      myDict <- PyDict.new
+      mapM_
+        (\(k, v) -> do
+          pyKey <- toPy k
+          pyVal <- toPy v
+          PyDict.setItem myDict pyKey pyVal)
+        dict
+      pure myDict
+
+setAttribute
+  :: ToPy a
+  => Text -- ^ module name
+  -> Text -- ^ attribute name
+  -> a -- ^ value to set attribute to
+  -> IO ()
+setAttribute moduleName name value = do
+  module_ <- importModule moduleName
+  pyName <- Py.toUnicode name
+  pyValue <- toPy value
+  Py.setAttribute module_ pyName pyValue
+
+getAttribute
+  :: FromPy a
+  => Text -- ^ module name
+  -> Text -- ^ attribute name
+  -> IO a
+getAttribute moduleName name = do
+  module_ <- importModule moduleName
+  attr <- Py.getAttribute module_ =<< Py.toUnicode name
+  fromPy attr
diff --git a/lib/CPython/Simple/Instances.hs b/lib/CPython/Simple/Instances.hs
new file mode 100644
--- /dev/null
+++ b/lib/CPython/Simple/Instances.hs
@@ -0,0 +1,119 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module CPython.Simple.Instances where
+
+import Control.Exception (Exception(..), throwIO)
+import Control.Monad ((<=<))
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Typeable
+
+import qualified CPython.Constants as Py
+import qualified CPython.Protocols.Object as Py
+import qualified CPython.Types as Py
+import qualified CPython.Types.Tuple as Py (fromTuple)
+
+-- TODO: ToPy/FromPy for Bool will require some library changes (e.g. adding fromBool)
+
+class ToPy a where
+  toPy :: a -> IO Py.SomeObject
+
+class FromPy a where
+  fromPy :: Py.SomeObject -> IO a
+
+data PyCastException = PyCastException String
+  deriving (Show)
+
+instance Exception PyCastException where
+  displayException (PyCastException typename) =
+    "FromPy could not cast to " ++ typename
+
+easyToPy
+  :: Py.Object c
+  => (a -> IO c) -- ^ python to- conversion, e.g. Py.toFloat
+  -> a           -- ^ haskell type being converted
+  -> IO Py.SomeObject
+easyToPy f = fmap Py.toObject . f
+
+easyFromPy
+  :: (Py.Concrete b, Typeable c)
+  => (b -> IO c)   -- ^ python from- conversion, e.g. Py.fromFloat
+  -> Proxy c       -- ^ proxy for the type being converted to
+  -> Py.SomeObject -- ^ python object to cast from
+  -> IO c
+easyFromPy conversion typename obj = do
+  casted <- Py.cast obj
+  case casted of
+    Nothing -> throwIO $ PyCastException (show $ typeRep typename)
+    Just x -> conversion x
+
+instance ToPy Integer where
+  toPy = easyToPy Py.toInteger
+
+instance FromPy Integer where
+  fromPy = easyFromPy Py.fromInteger Proxy
+
+instance ToPy Double where
+  toPy = easyToPy Py.toFloat
+
+instance FromPy Double where
+  fromPy = easyFromPy Py.fromFloat Proxy
+
+instance ToPy Text where
+  toPy = easyToPy Py.toUnicode
+
+instance FromPy Text where
+  fromPy = easyFromPy Py.fromUnicode Proxy
+
+instance ToPy Char where
+  toPy = easyToPy Py.toUnicode . T.singleton
+
+instance FromPy Char where
+  fromPy c = T.head <$> easyFromPy Py.fromUnicode Proxy c
+
+instance ToPy String where
+  toPy = easyToPy Py.toUnicode . T.pack
+
+instance FromPy String where
+  fromPy s = T.unpack <$> easyFromPy Py.fromUnicode Proxy s
+
+instance (FromPy a, FromPy b) => FromPy (a, b) where
+  fromPy val = do
+    [pyA, pyB] <- easyFromPy Py.fromTuple Proxy val
+    a <- fromPy pyA
+    b <- fromPy pyB
+    pure (a, b)
+
+instance (FromPy a, FromPy b, FromPy c) => FromPy (a, b, c) where
+  fromPy val = do
+    [pyA, pyB, pyC] <- easyFromPy Py.fromTuple Proxy val
+    a <- fromPy pyA
+    b <- fromPy pyB
+    c <- fromPy pyC
+    pure (a, b, c)
+
+instance FromPy a => FromPy (Maybe a) where
+  fromPy val = do
+    isNone <- Py.isNone val
+    if isNone
+      then pure Nothing
+      else Just <$> fromPy val
+
+instance ToPy a => ToPy (Maybe a) where
+  toPy Nothing = Py.none
+  toPy (Just a) = toPy a
+
+instance FromPy a => FromPy [a] where
+  fromPy val = do
+    list <- easyFromPy Py.fromList Proxy val
+    mapM fromPy list
+
+instance ToPy a => ToPy [a] where
+  toPy val = do
+    list <- mapM toPy val
+    Py.toObject <$> Py.toList list
+
+instance FromPy () where
+  fromPy _ = pure ()
