diff --git a/src/python/PyBis/README.md b/src/python/PyBis/README.md
index 34b4c698bd8844b81caabc829982cc9410d43d23..b4db8bb03b8caf6dba85e80ce2473e86f8659e95 100644
--- a/src/python/PyBis/README.md
+++ b/src/python/PyBis/README.md
@@ -288,6 +288,49 @@ tag.get_samples()
 tag.delete()
 ```
 
+## Vocabualry and VocabularyTerms
+
+Vocabulary consists of many VocabularyTerms. They are used to control the Terms in a Property field. So for example, you want to add a property called **Animal** to a Sample and you want to control which terms are used in this Property. For this you need to do a couple of steps:
+
+1. create a new vocabulary *AnimalVocabulary*
+2. add terms to that vocabulary: *Cat, Dog, Mouse*
+3. create a new PropertyType (e.g. *Animal*) of DataType *CONTROLLEDVOCABULARY* and assign the *AnimalVocabulary* to it
+4. create a new SampleType (e.g. *Pet*) and *assign* the created PropertyType to that Sample type.
+5. If you now create a new Sample of type *Pet* you will be able to add a property *Animal* to it which only accepts the terms *Cat, Dog* or *Mouse*.
+
+
+**create new Vocabulary with three VocabularyTerms**
+
+```
+voc = o.new_vocabulary(
+    code = 'BBB',
+    description = 'description of vocabulary aaa',
+    urlTemplate = 'https://ethz.ch',
+    terms = [
+        { "code": 'term_code1', "label": "term_label1", "description": "term_description1"},
+        { "code": 'term_code2', "label": "term_label2", "description": "term_description2"},
+        { "code": 'term_code3', "label": "term_label3", "description": "term_description3"}
+    ]   
+)
+voc.save()
+```
+
+**create additional VocabularyTerms**
+
+```
+term = o.new_term(
+	code='TERM_CODE_XXX', 
+	vocabularyCode='BBB', 
+	label='here comes a label',
+	description='here is a meandingful description'
+)
+term.save()
+```
+
+**fetching Vocabulary and VocabularyTerms**
+
+
+
 
 # Requirements and organization
 
diff --git a/src/python/PyBis/pybis/attribute.py b/src/python/PyBis/pybis/attribute.py
index 5500dc4a4d0970863fe709bf959c866f718c1be7..8ff071d3453876ec8c02f835d32ecc14c7354224 100644
--- a/src/python/PyBis/pybis/attribute.py
+++ b/src/python/PyBis/pybis/attribute.py
@@ -59,6 +59,9 @@ class AttrHolder():
                 if isinstance(self.__dict__['_' + attr], dict):
                     self.__dict__['_' + attr].pop('@id')
 
+            elif attr == 'vocabularyCode':
+                self.__dict__['_'+attr] = data.get('permId', {}).get(attr, None)
+
             elif attr in ["space"]:
                 d = data.get(attr, None)
                 if d is not None:
@@ -239,10 +242,10 @@ class AttrHolder():
                     "@type": "as.dto.common.update.IdListUpdateValue" 
                 }
 
-            elif attr == 'description':
-                # alway update description
-                up_obj['description'] = {
-                    "value": self.__dict__['_description'],
+            elif attr in 'description label official ordinal'.split():
+                # alway update common fields
+                up_obj[attr] = {
+                    "value": self.__dict__['_'+attr],
                     "isModified": True,
                     "@type": "as.dto.common.update.FieldUpdateValue"
                 }
@@ -269,7 +272,7 @@ class AttrHolder():
                     value = self.__dict__.get('_' + attr, {})
                     if value is None:
                         pass
-                    elif len(value) == 0:
+                    elif isinstance(value, dict) and len(value) == 0:
                         # value is {}: it means that we want this attribute to be
                         # deleted, not updated.
                         up_obj[attr2ids[attr]] = {
diff --git a/src/python/PyBis/pybis/definitions.py b/src/python/PyBis/pybis/definitions.py
index 583838d41d7f948bc19e89310453947356b60c0b..81bd2c1e426879de0044b60319ddf9d130f0a354 100644
--- a/src/python/PyBis/pybis/definitions.py
+++ b/src/python/PyBis/pybis/definitions.py
@@ -1,4 +1,12 @@
 def openbis_definitions(entity):
+    """
+    attrs_new: Attributes, that can appear when creating new entities
+    attrs_up: Attributes that can be updated
+    attrs: Attributes that are displayed when fetched
+    multi: multivalue-elements which appear in an entity. E.g. parents or children in a Sample.
+    identifier: to update entities, the identifier must be specified. Usually identityName + "Id"
+    (Entity-Name in camel-case, starting with lowercase letter, with «Id» added)
+    """
     entities = {
         "Space": {
             "attrs_new": "code description".split(),
@@ -6,6 +14,10 @@ def openbis_definitions(entity):
             "attrs": "code permId description registrator registrationDate modificationDate".split(),
             "multi": "".split(),
             "identifier": "spaceId",
+            "create": { "@type": "as.dto.space.create.SpaceCreation"},
+            "update": { "@type": "as.dto.space.upate.SpaceUpdate"},
+            "delete": { "@type": "as.dto.space.delete.SpaceDeletionOptions"},
+            "fetch":  { "@type": "as.dto.space.fetchoptions.SpaceFetchOptions"},
         },
         "Project": {
             "attrs_new": "code description space attachments".split(),
@@ -78,6 +90,28 @@ def openbis_definitions(entity):
             "multi": "".split(),
             "identifier": "tagId",
         },
+        "Vocabulary": {
+            "attrs_new": "code description managedInternally internalNameSpace chosenFromList urlTemplate".split(),
+            "attrs_up": "description managedInternally internalNameSpace chosenFromList urlTemplate".split(),
+            "attrs": "code description managedInternally internalNameSpace chosenFromList urlTemplate registrator registrationDate modificationDate".split(),
+            "multi": "".split(),
+            "identifier": "vocabularyId",
+            "create": { "@type": "as.dto.vocabulary.create.VocabularyCreation"}, 
+            "update": { "@type": "as.dto.vocabulary.upate.VocabularyUpdate"},
+            "delete": { "@type": "as.dto.vocabulary.delete.VocabularyDeletionOptions"},
+            "fetch":  { "@type": "as.dto.vocabulary.fetchoptions.VocabularyFetchOptions"},
+        },
+        "VocabularyTerm": {
+            "attrs_new": "code vocabularyCode label description official ordinal".split(),
+            "attrs_up": "label description official ordinal".split(),
+            "attrs": "code vocabularyCode label description official ordinal registrationDate modificationDate registrator".split(),
+            "multi": "".split(),
+            "identifier": "vocabularyTermId",
+            "create": { "@type": "as.dto.vocabulary.create.VocabularyTermCreation"},
+            "update": { "@type": "as.dto.vocabulary.upate.VocabularyTermUpdate"},
+            "delete": { "@type": "as.dto.vocabulary.delete.VocabularyTermDeletionOptions"},
+            "fetch":  { "@type": "as.dto.vocabulary.fetchoptions.VocabularyTermFetchOptions"},
+        },
         "Plugin": {
             "attrs_new": "name description available script available script pluginType pluginKind entityKinds".split(),
             "attrs_up": "description, available script available script pluginType pluginKind entityKinds".split(),
@@ -133,6 +167,8 @@ def openbis_definitions(entity):
     }
     return entities[entity]
 
+get_definition_for_entity = openbis_definitions   # Alias
+
 
 fetch_option = {
     "space": {"@type": "as.dto.space.fetchoptions.SpaceFetchOptions"},
@@ -195,4 +231,31 @@ fetch_option = {
     "history": {"@type": "as.dto.history.fetchoptions.HistoryEntryFetchOptions"},
     "dataStore": {"@type": "as.dto.datastore.fetchoptions.DataStoreFetchOptions"},
     "plugin": {"@type": "as.dto.plugin.fetchoptions.PluginFetchOptions"},
+    "vocabulary": {
+        "@type": "as.dto.vocabulary.fetchoptions.VocabularyFetchOptions",
+        "terms": {
+            "@type": "as.dto.vocabulary.fetchoptions.VocabularyTermFetchOptions"
+        },
+    },
+    "vocabularyTerm": {"@type": "as.dto.vocabulary.fetchoptions.VocabularyTermFetchOptions"},
 }
+
+def get_fetchoption_for_entity(entity):
+    entity = entity[0].lower() + entity[1:]   # make first character lowercase
+    try:
+        return fetch_option[entity]
+    except KeyError as e:
+        return {}
+
+def get_type_for_entity(entity, action):
+    if action not in "create update delete fetch".split():
+        raise ValueError('unknown action: {}'.format(action))
+
+    definition = openbis_definitions(entity)
+    return definition[action]
+
+def get_method_for_entity(entity, action):
+    if action == "Vocabulary":
+        return "{}Vocabularies".format(action)
+
+    return "{}{}s".format(action, entity)
diff --git a/src/python/PyBis/pybis/pybis.py b/src/python/PyBis/pybis/pybis.py
index 3657e3b8e50ed8e2bf3ec0409f578b45afc889df..979071b985dd6e18135ce5eef7705518d7c0c7e9 100644
--- a/src/python/PyBis/pybis/pybis.py
+++ b/src/python/PyBis/pybis/pybis.py
@@ -28,11 +28,11 @@ from tabulate import tabulate
 
 from . import data_set as pbds
 from .utils import parse_jackson, check_datatype, split_identifier, format_timestamp, is_identifier, is_permid, nvl, VERBOSE
-from .utils import extract_permid, extract_code,extract_deletion,extract_identifier,extract_nested_identifier,extract_nested_permid,extract_property_assignments,extract_role_assignments,extract_person, extract_person_details,extract_id,extract_userId
+from .utils import extract_attr, extract_permid, extract_code,extract_deletion,extract_identifier,extract_nested_identifier,extract_nested_permid,extract_property_assignments,extract_role_assignments,extract_person, extract_person_details,extract_id,extract_userId
 from .property import PropertyHolder, PropertyAssignments
 from .vocabulary import Vocabulary, VocabularyTerm
 from .openbis_object import OpenBisObject 
-from .definitions import fetch_option
+from .definitions import get_definition_for_entity, fetch_option, get_fetchoption_for_entity, get_type_for_entity, get_method_for_entity
 
 # import the various openBIS entities
 from .things import Things
@@ -555,6 +555,9 @@ class Openbis:
             "get_tag(tagId)",
             "new_tag(code, description)",
             "get_terms()",
+            "get_term()",
+            "get_vocabularies()",
+            "get_vocabulary()",
             "new_person(userId, space)",
             "get_persons()",
             "get_person(userId)",
@@ -1623,7 +1626,24 @@ class Openbis:
                 }
             ]
         }
-        self._post_request(self.as_v3, request)
+        resp = self._post_request(self.as_v3, request)
+
+
+    def delete_openbis_entity(self, entity, objectId, reason='No reason given'):
+        method = get_method_for_entity(entity, 'delete')
+        delete_options = get_type_for_entity(entity, 'delete')
+        delete_options['reason'] = reason
+
+        request = {
+           "method": method,
+           "params": [
+                self.token,
+                [ objectId ],
+                delete_options
+            ]
+        }
+        resp = self._post_request(self.as_v3, request)
+        return
 
 
     def get_deletions(self):
@@ -1774,7 +1794,8 @@ class Openbis:
         return request
 
     def get_terms(self, vocabulary=None):
-        """ Returns information about vocabulary, including its controlled vocabulary
+        """ Returns information about existing vocabulary terms. 
+        If a vocabulary code is provided, it only returns the terms of that vocabulary.
         """
 
         search_request = {}
@@ -1787,30 +1808,139 @@ class Openbis:
                 }]
             })
 
-        fetch_options = {
-            "vocabulary": {"@type": "as.dto.vocabulary.fetchoptions.VocabularyFetchOptions"},
-            "@type": "as.dto.vocabulary.fetchoptions.VocabularyTermFetchOptions"
-        }
+        fetchopts = fetch_option['vocabularyTerm']
 
         request = {
             "method": "searchVocabularyTerms",
-            "params": [self.token, search_request, fetch_options]
+            "params": [self.token, search_request, fetchopts]
         }
         resp = self._post_request(self.as_v3, request)
 
-        attrs = 'code label description vocabulary registrationDate modificationDate official ordinal'.split()
+        attrs = 'code vocabularyCode label description registrationDate modificationDate official ordinal'.split()
 
         if len(resp['objects']) == 0:
             terms = DataFrame(columns=attrs)
         else:
             objects = resp['objects']
-            parse_jackson(resp)
+            parse_jackson(objects)
             terms = DataFrame(objects)
+            terms['vocabularyCode'] = terms['permId'].map(extract_attr('vocabularyCode'))
             terms['registrationDate'] = terms['registrationDate'].map(format_timestamp)
             terms['modificationDate'] = terms['modificationDate'].map(format_timestamp)
 
-        return Things(self, 'term', terms[attrs], 'permId')
-        return VocabularyTerm(terms)
+        return Things(self, 'term', terms[attrs], 
+            identifier_name='code', additional_identifier='vocabularyCode')
+        
+
+    def new_term(self, code, vocabularyCode, label=None, description=None):
+        return VocabularyTerm(
+            self, data=None, 
+            code=code, vocabularyCode=vocabularyCode, 
+            label=label, description=description
+        )
+
+
+    def get_term(self, code, vocabularyCode=None, only_data=False):
+        entity_def = get_definition_for_entity('VocabularyTerm')
+        search_request = {
+            "code": code,
+            "vocabularyCode": vocabularyCode,
+            "@type": "as.dto.vocabulary.id.VocabularyTermPermId"
+        }
+        fetchopts = get_fetchoption_for_entity('VocabularyTerm')
+        for opt in ['registrator']:
+            fetchopts[opt] = get_fetchoption_for_entity(opt)
+        
+        request = {
+            "method": 'getVocabularyTerms',
+            "params": [
+                self.token,
+                [search_request],
+                fetchopts
+            ],
+        }
+        resp = self._post_request(self.as_v3, request)
+
+        if resp is None or len(resp) == 0:
+            raise ValueError("no VocabularyTerm found with code='{}' and vocabularyCode='{}'".format(code, vocabularyCode))
+        else:
+            parse_jackson(resp)
+            for ident in resp:
+                if only_data:
+                    return resp[ident]
+                else:
+                    return VocabularyTerm(self, resp[ident])
+
+
+    def get_any_entity(self, identifier, entity, only_data=False, method=None):
+
+        entity_def = get_definition_for_entity(entity)
+
+        # guess the method to fetch an entity
+        if method is None:
+            method = 'get' + entity + 's'
+
+        search_request = search_request_for_identifier(identifier, entity)
+        fetchopts = get_fetchoption_for_entity(entity)
+        
+        request = {
+            "method": method,
+            "params": [
+                self.token,
+                [search_request],
+                fetchopts
+            ],
+        }
+        resp = self._post_request(self.as_v3, request)
+
+        if resp is None or len(resp) == 0:
+            raise ValueError('no {} found with identifier: {}'.format(entity, identifier))
+        else:
+            parse_jackson(resp)
+            for ident in resp:
+                if only_data:
+                    return resp[ident]
+                else:
+                    # get a dynamic instance of that class
+                    klass = globals()[entity]
+                    return klass(self, resp[ident])
+
+
+    def get_vocabularies(self):
+        """ Returns information about vocabulary
+        """
+
+        search_request = {}
+
+        fetchopts = fetch_option['vocabulary']
+        for option in ['registrator']:
+            fetchopts[option] = fetch_option[option]
+
+        request = {
+            "method": "searchVocabularies",
+            "params": [self.token, search_request, fetchopts]
+        }
+        resp = self._post_request(self.as_v3, request)
+
+        attrs = 'code description managedInternally internalNameSpace chosenFromList urlTemplate registrator registrationDate modificationDate'.split()
+
+        if len(resp['objects']) == 0:
+            vocs = DataFrame(columns=attrs)
+        else:
+            objects = resp['objects']
+            parse_jackson(resp)
+            vocs = DataFrame(objects)
+            vocs['registrationDate'] = vocs['registrationDate'].map(format_timestamp)
+            vocs['modificationDate'] = vocs['modificationDate'].map(format_timestamp)
+            vocs['registrator']      = vocs['registrator'].map(extract_person)
+
+        return Things(self, 'vocabulary', vocs[attrs], 'code')
+
+
+    def get_vocabulary(self, code, only_data=False):
+        """ Returns the details of a given vocabulary (including vocabulary terms)
+        """
+        return self.get_any_entity(code, 'Vocabulary', only_data=only_data, method='getVocabularies')
 
 
     def new_tag(self, code, description=None):
@@ -2275,7 +2405,7 @@ class Openbis:
         for key in ['parents','children','container','components']:
             fetchopts[key] = {"@type": "as.dto.sample.fetchoptions.SampleFetchOptions"}
 
-        sample_request = {
+        request = {
             "method": "getSamples",
             "params": [
                 self.token,
@@ -2284,7 +2414,7 @@ class Openbis:
             ],
         }
 
-        resp = self._post_request(self.as_v3, sample_request)
+        resp = self._post_request(self.as_v3, request)
         parse_jackson(resp)
 
         if resp is None or len(resp) == 0:
@@ -2449,6 +2579,26 @@ class Openbis:
             entityType=entityType, propertyType=propertyType, **kwargs
         )    
 
+    def new_vocabulary(self, code, terms, managedInternally=False, internalNameSpace=False, chosenFromList=True, **kwargs):
+        """ Creates a new vocabulary
+        Usage::
+            new_vocabulary(
+                code = 'vocabulary_code',
+                description = '',
+                terms = [
+                    { "code": "term1", "label": "label1", "description": "description1" },
+                    { "code": "term2", "label": "label2", "description": "description2" },
+                ]
+            )
+        """
+        kwargs['code'] = code
+        kwargs['managedInternally'] = managedInternally
+        kwargs['internalNameSpace'] = internalNameSpace
+        kwargs['chosenFromList'] = chosenFromList
+        return Vocabulary(self, data=None, terms=terms, **kwargs)
+
+        
+
     def _get_dss_url(self, dss_code=None):
         """ internal method to get the downloadURL of a datastore.
         """
diff --git a/src/python/PyBis/pybis/space.py b/src/python/PyBis/pybis/space.py
index 742f633326fdbb0b1d1062e19a7c5042b4ab764f..1be04c0d6d7a805bb7d9a5ddd648f1f139bfaa5c 100644
--- a/src/python/PyBis/pybis/space.py
+++ b/src/python/PyBis/pybis/space.py
@@ -57,7 +57,7 @@ class Space(OpenBisObject):
         return self.openbis.new_sample(space=self, **kwargs)
 
     def delete(self, reason):
-        self.openbis.delete_entity('Space', self.permId, reason)
+        self.openbis.delete_entity(entity='Space', id=self.permId, reason=reason)
         if VERBOSE: print("Space {} has been sucsessfully deleted.".format(self.permId))
 
     def save(self):
diff --git a/src/python/PyBis/pybis/things.py b/src/python/PyBis/pybis/things.py
index eceb421dc27340fedc5054decf0a80d75b298ec5..131ea6baccb075b5406fa4f9cd7fab09f244b0c5 100644
--- a/src/python/PyBis/pybis/things.py
+++ b/src/python/PyBis/pybis/things.py
@@ -5,11 +5,13 @@ class Things():
        
     """
 
-    def __init__(self, openbis_obj, entity, df, identifier_name='code'):
+    def __init__(self, openbis_obj, entity, df, 
+        identifier_name='code', additional_identifier=None):
         self.openbis = openbis_obj
         self.entity = entity
         self.df = df
         self.identifier_name = identifier_name
+        self.additional_identifier = additional_identifier
 
     def __repr__(self):
         return tabulate(self.df, headers=list(self.df))
@@ -101,6 +103,11 @@ class Things():
                 return Things(self.openbis, 'dataset', DataFrame(), 'permId')
 
     def __getitem__(self, key):
+        """ elegant way to fetch a certain element from the displayed list.
+        If an integer value is given, we choose the row.
+        If the key is a list, we return the desired columns (normal dataframe behaviour)
+        If the key is a non-integer value, we treat it as a primary-key lookup
+        """
         if self.df is not None and len(self.df) > 0:
             row = None
             if isinstance(key, int):
@@ -115,7 +122,14 @@ class Things():
 
             if row is not None:
                 # invoke the openbis.get_<entity>() method
-                return getattr(self.openbis, 'get_' + self.entity)(row[self.identifier_name].values[0])
+                if self.additional_identifier is None:
+                    return getattr(self.openbis, 'get_' + self.entity)(row[self.identifier_name].values[0])
+                ## get an entry using two keys
+                else:
+                    return getattr(self.openbis, 'get_' + self.entity)(
+                            row[self.identifier_name].values[0],
+                            row[self.additional_identifier].values[0]
+                    )
 
     def __iter__(self):
         for item in self.df[[self.identifier_name]][self.identifier_name].iteritems():
diff --git a/src/python/PyBis/pybis/utils.py b/src/python/PyBis/pybis/utils.py
index 6f78cf181e2e3543d802d61dbad4ba236365bbf5..bb9a32240bdfa160453f6dba0edf72e261bd1223 100644
--- a/src/python/PyBis/pybis/utils.py
+++ b/src/python/PyBis/pybis/utils.py
@@ -153,12 +153,12 @@ def extract_deletion(obj):
 
 
 def extract_attr(attr):
-    def attr(obj):
+    def attr_func(obj):
         if isinstance(obj, dict):
             return obj.get(attr, '')
         else:
             return str(obj)
-    return attr 
+    return attr_func
 
 
 def extract_identifier(ident):
diff --git a/src/python/PyBis/pybis/vocabulary.py b/src/python/PyBis/pybis/vocabulary.py
index cefb8368729af2d846fa1993c2597a577a930769..d97c4aa2f675bc44dbef6e5cccc69028b9b8347c 100644
--- a/src/python/PyBis/pybis/vocabulary.py
+++ b/src/python/PyBis/pybis/vocabulary.py
@@ -1,63 +1,187 @@
 from .utils import VERBOSE
 from .openbis_object import OpenBisObject 
+from .attribute import AttrHolder
+from .definitions import openbis_definitions, fetch_option
+import json
 
-class Vocabulary():
+class Vocabulary(OpenBisObject):
 
-    def __init__(self, data):
-        self.data = data
+    def __init__(self, openbis_obj, data=None, terms=None, **kwargs):
+        self.__dict__['openbis'] = openbis_obj
+        self.__dict__['a'] = AttrHolder(openbis_obj, 'Vocabulary')
+
+        if data is not None:
+            self._set_data(data)
+            self.__dict__['terms'] = data['terms']
+
+        if terms is not None:
+            self.__dict__['terms'] = terms
+
+        if self.is_new:
+            allowed_attrs = openbis_definitions(self.entity)['attrs_new']
+            for key in kwargs:
+                if key not in allowed_attrs:
+                    raise ValueError(
+                        "{} is an unknown Vocabulary attribute. Allowed attributes are: {}".format(
+                            key, ", ".join(allowed_attrs) 
+                        ) 
+                    )
+
+        if kwargs is not None:
+            for key in kwargs:
+                setattr(self, key, kwargs[key])
+
+
+    def get_terms(self):
+        """ Returns the VocabularyTerms of the given Vocabulary.
+        """
+        return self.openbis.get_terms(vocabulary=self.code)
+
+    def add_term(self, code, label=None, description=None):
+        """ Adds a term to this Vocabulary.
+        If Vocabulary is already persistent, it is added by adding a new VocabularyTerm object.
+        If Vocabulary is new, the term is added to the list of terms
+        """
+        if self.is_new:
+            self.__dict__['terms'].append({
+                "code": code,
+                "label": label,
+                "description": description
+            })
+        else:
+            pass
+        
+
+    def save(self):
+        if self.is_new:
+            request = self._new_attrs('createVocabularies')
+            # add the VocabularyTerm datatype
+            terms = self.__dict__['terms']
+            for term in terms:
+                term["@type"]= "as.dto.vocabulary.create.VocabularyTermCreation"
+            request['params'][1][0]['terms'] = terms 
+            resp = self.openbis._post_request(self.openbis.as_v3, request)
+
+            if VERBOSE: print("Vocabulary successfully created.")
+            data = self.openbis.get_vocabulary(resp[0]['permId'], only_data=True)
+            self._set_data(data)
+            return self
+
+        else:
+            request = self._up_attrs('updateVocabularies')
+            self.openbis._post_request(self.openbis.as_v3, request)
+            if VERBOSE: print("Vocabulary successfully updated.")
+            data = self.openbis.get_vocabulary(self.permId, only_data=True)
+            self._set_data(data)
+
+
+class VocabularyTerm(OpenBisObject):
+
+    def __init__(self, openbis_obj, data=None, **kwargs):
+        self.__dict__['openbis'] = openbis_obj
+        self.__dict__['a'] = AttrHolder(openbis_obj, 'VocabularyTerm')
+
+
+        if data is not None:
+            self._set_data(data)
+
+        if kwargs is not None:
+            for key in kwargs:
+                setattr(self, key, kwargs[key])
 
-    @property
-    def terms_kv(self):
-         return [ 
-            {voc["code"]:voc["label"] }
-            for voc 
-            in sorted(self.data['objects'], key=lambda v: v["ordinal"]) 
-        ]
 
     @property
-    def terms(self):
-         return [ 
-            voc["code"]
-            for voc 
-            in sorted(self.data['objects'], key=lambda v: v["ordinal"]) 
-        ]
-
-
-    def _repr_html_(self):
-        html = """
-            <table border="1" class="dataframe">
-            <thead>
-                <tr style="text-align: right;">
-                <th>vocabulary term</th>
-                <th>label</th>
-                <th>description</th>
-                <th>vocabulary</th>
-                </tr>
-            </thead>
-            <tbody>
+    def vocabularyCode(self):
+        if self.is_new:
+            return self.__dict__['a'].vocabularyCode
+        else:
+            return self.data['permId']['vocabularyCode']
+
+
+    def _up_attrs(self):
+        """ AttributeTerms behave quite differently to all other openBIS entities,
+        that's why we need to override this method
         """
+        attrs = {}
+        for attr in 'label description'.split():
+            attrs[attr] = {
+                "value": getattr(self, attr),
+                "isModified": True,
+                "@type": "as.dto.common.update.FieldUpdateValue"
+            }
 
-        for voc in sorted(
-            self.data['objects'], 
-            key=lambda v: (v["permId"]["vocabularyCode"], v["ordinal"])
-        ):
+        attrs["vocabularyTermId"] = self.vocabularyTermId()
+        attrs["@type"] = "as.dto.vocabulary.update.VocabularyTermUpdate"
+        request = {
+            "method": "updateVocabularyTerms",
+            "params": [
+                self.openbis.token,
+                [attrs]
+            ]
+        }
+        return request
 
-            html += "<tr> <td>{}</td> <td>{}</td> <td>{}</td> <td>{}</td> </tr>".format(
-                voc['code'],
-                voc['label'],
-                voc['description'],
-                voc['permId']['vocabularyCode']
-            )
 
-        html += """
-            </tbody>
-            </table>
+    def _new_attrs(self):
+        attrs = {
+            "@type": "as.dto.vocabulary.create.VocabularyTermCreation",
+            "vocabularyId": self.vocabularyTermId()
+        }
+        for attr in 'code label description'.split():
+            attrs[attr] = getattr(self, attr)
+
+        request = {
+            "method": "createVocabularyTerms",
+            "params": [
+                self.openbis.token,
+                [attrs]
+            ]
+        }
+        return request
+
+
+    def vocabularyTermId(self):
+        """ needed for updating a term.
         """
-        return html
+        if self.is_new:
+            return {
+                "permId": getattr(self, 'vocabularyCode'),
+                "@type": "as.dto.vocabulary.id.VocabularyPermId"
+            }
+        else:
+            permId = self.data['permId']
+            permId.pop('@id', None)
+            return permId
 
 
-class VocabularyTerm(OpenBisObject):
+    def save(self):
+        if self.is_new:
+            request = self._new_attrs()
+            resp = self.openbis._post_request(self.openbis.as_v3, request)
+
+            if VERBOSE: print("Vocabulary Term successfully created.")
+            data = self.openbis.get_term(
+                code=resp[0]['code'], 
+                vocabularyCode=resp[0]['vocabularyCode'],
+                only_data=True
+            )
+            self._set_data(data)
+            return self
+
+        else:
+            request = self._up_attrs()
+            self.openbis._post_request(self.openbis.as_v3, request)
+            if VERBOSE: print("Vocabulary Term successfully updated.")
+            data = self.openbis.get_term(
+                code=self.code, 
+                vocabularyCode=self.vocabularyCode,
+                only_data=True
+            )
+            self._set_data(data)
 
-    def __init__(self, data):
-        self.data = data
 
+    def delete(self, reason='no particular reason'):
+        self.openbis.delete_openbis_entity(
+            entity='VocabularyTerm', objectId=self.data['permId'], reason=reason
+        )
+        if VERBOSE: print("VocabularyTerm successfully deleted.")
diff --git a/src/python/PyBis/tests/test_vocabulary.py b/src/python/PyBis/tests/test_vocabulary.py
new file mode 100644
index 0000000000000000000000000000000000000000..6ab6145bbb56cc1ffe18d71d12ddf18bcb5db072
--- /dev/null
+++ b/src/python/PyBis/tests/test_vocabulary.py
@@ -0,0 +1,18 @@
+import json
+import random
+import re
+
+import pytest
+import time
+
+
+def test_create_delete_vocabulay_terms(openbis_instance):
+    o=openbis_instance 
+
+    terms = o.get_terms()
+    assert terms is not None
+    assert terms.df is not None
+    
+
+
+
diff --git a/src/vagrant/jupyter-bis/README.md b/src/vagrant/jupyter-bis/README.md
index 8294a53f8ce3c44bff687b9f873ee7b435af2aed..4be7e4122be0934c35d6c2bbf957cad8d9656ff3 100644
--- a/src/vagrant/jupyter-bis/README.md
+++ b/src/vagrant/jupyter-bis/README.md
@@ -83,6 +83,7 @@ This process describes how to upgrade your openBIS instance in your virtual mach
 10. do the openBIS upgrade: `bin/upgrade.sh`
 11. switch back to vagrant user: `logout` or CTRL-D
 11. start openBIS: `sync/initialize/start_services.sh`
+12. optionally delete previous installations (to save diskspace): `rm -rf backup/*`
 
 ## start openBIS and JupyterHub